diff --git a/package.json b/package.json
index effe186..dfdfdf5 100644
--- a/package.json
+++ b/package.json
@@ -13,10 +13,12 @@
"@ethereumjs/common": "^4.3.0",
"@ethereumjs/tx": "^5.3.0",
"@ethereumjs/util": "^9.0.3",
- "@near-wallet-selector/core": "^8.9.5",
- "@near-wallet-selector/here-wallet": "^8.9.5",
- "@near-wallet-selector/modal-ui": "^8.9.5",
- "@near-wallet-selector/my-near-wallet": "^8.9.5",
+ "@near-wallet-selector/core": "^8.9.10",
+ "@near-wallet-selector/here-wallet": "^8.9.10",
+ "@near-wallet-selector/meteor-wallet": "^8.9.10",
+ "@near-wallet-selector/modal-ui": "^8.9.10",
+ "@near-wallet-selector/my-near-wallet": "^8.9.10",
+ "@vitejs/plugin-react": "^4.2.1",
"axios": "^1.6.8",
"bitcoinjs-lib": "^6.1.5",
"bn.js": "^5.2.1",
@@ -24,15 +26,16 @@
"elliptic": "^6.5.5",
"ethers": "^6.11.1",
"hash.js": "^1.1.7",
+ "js-sha3": "^0.9.3",
"keccak": "^3.0.4",
"near-api-js": "^3.0.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "^7.8.1",
+ "viem": "^2.18.4",
"vite-plugin-node-polyfills": "^0.21.0",
- "web3": "^4.6.0",
- "@vitejs/plugin-react": "^4.2.1"
+ "web3": "^4.6.0"
},
"overrides": {
"near-api-js": "^3.0.4"
diff --git a/src/App.jsx b/src/App.jsx
index 64e1e56..a257f30 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,10 +7,14 @@ import { EthereumView } from "./components/Ethereum/Ethereum";
import { BitcoinView } from "./components/Bitcoin";
// CONSTANTS
-const MPC_CONTRACT = 'v2.multichain-mpc.testnet';
+const MPC_CONTRACT = 'v1.signer-prod.testnet';
// NEAR WALLET
-const wallet = new Wallet({ network: 'testnet', createAccessKeyFor: MPC_CONTRACT });
+const wallet = new Wallet({ network: 'testnet' });
+
+// parse transactionHashes from URL
+const txHash = new URLSearchParams(window.location.search).get('transactionHashes');
+const transactions = txHash ? txHash.split(',') : [];
function App() {
const [signedAccountId, setSignedAccountId] = useState('');
@@ -43,8 +47,8 @@ function App() {
- {chain === 'eth' && }
- {chain === 'btc' && }
+ {chain === 'eth' && }
+ {chain === 'btc' && }
}
diff --git a/src/components/Ethereum/Ethereum.jsx b/src/components/Ethereum/Ethereum.jsx
index 4103f5f..ba9eddc 100644
--- a/src/components/Ethereum/Ethereum.jsx
+++ b/src/components/Ethereum/Ethereum.jsx
@@ -11,37 +11,60 @@ import { FunctionCallForm } from "./FunctionCall";
const Sepolia = 11155111;
const Eth = new Ethereum('https://rpc2.sepolia.org', Sepolia);
-export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
+export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } }) {
const { wallet, signedAccountId } = useContext(NearContext);
const [loading, setLoading] = useState(false);
- const [step, setStep] = useState("request");
+ const [step, setStep] = useState(transactions ? 'relay' : "request");
const [signedTransaction, setSignedTransaction] = useState(null);
+
+ const [senderLabel, setSenderLabel] = useState("")
const [senderAddress, setSenderAddress] = useState("")
const [action, setAction] = useState("transfer")
- const [derivation, setDerivation] = useState("ethereum-1");
- const derivationPath = useDebounce(derivation, 1000);
+ const [derivation, setDerivation] = useState(sessionStorage.getItem('derivation') || "ethereum-1");
+ const derivationPath = useDebounce(derivation, 1200);
+
+ const [reloaded, setReloaded] = useState(transactions.length? true : false);
const childRef = useRef();
useEffect(() => {
- setSenderAddress('Waiting for you to stop typing...')
+ // special case for web wallet that reload the whole page
+ if (reloaded && senderAddress) signTransaction()
+
+ async function signTransaction() {
+ const { big_r, s, recovery_id } = await wallet.getTransactionResult(transactions[0]);
+ console.log({ big_r, s, recovery_id });
+ const signedTransaction = await Eth.reconstructSignatureFromLocalSession(big_r, s, recovery_id, senderAddress);
+ setSignedTransaction(signedTransaction);
+ setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`);
+ setStep('relay');
+
+ setReloaded(false);
+ removeUrlParams();
+ }
+
+ }, [senderAddress]);
+
+ useEffect(() => {
+ setSenderLabel('Waiting for you to stop typing...')
+ setStatus('Querying Ethereum address and Balance...');
+ setSenderAddress(null)
+ setStep('request');
}, [derivation]);
useEffect(() => {
setEthAddress()
-
+ console.log(derivationPath)
async function setEthAddress() {
- setStatus('Querying your address and balance');
- setSenderAddress(`Deriving address from path ${derivationPath}...`);
-
const { address } = await Eth.deriveAddress(signedAccountId, derivationPath);
setSenderAddress(address);
+ setSenderLabel(address);
const balance = await Eth.getBalance(address);
- setStatus(`Your Ethereum address is: ${address}, balance: ${balance} ETH`);
+ if (!reloaded) setStatus(`Your Ethereum address is: ${address}, balance: ${balance} ETH`);
}
- }, [signedAccountId, derivationPath, setStatus]);
+ }, [derivationPath]);
async function chainSignature() {
setStatus('🏗️ Creating transaction');
@@ -51,7 +74,9 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
setStatus(`🕒 Asking ${MPC_CONTRACT} to sign the transaction, this might take a while`);
try {
- const signedTransaction = await Eth.requestSignatureToMPC(wallet, MPC_CONTRACT, derivationPath, payload, transaction, senderAddress);
+ const { big_r, s, recovery_id } = await Eth.requestSignatureToMPC(wallet, MPC_CONTRACT, derivationPath, payload, transaction, senderAddress);
+ const signedTransaction = await Eth.reconstructSignature(big_r, s, recovery_id, transaction, senderAddress);
+
setSignedTransaction(signedTransaction);
setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`);
setStep('relay');
@@ -61,12 +86,10 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
}
}
-
-
async function relayTransaction() {
setLoading(true);
setStatus('🔗 Relaying transaction to the Ethereum network... this might take a while');
-
+
try {
const txHash = await Eth.relayTransaction(signedTransaction);
setStatus(
@@ -95,7 +118,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
setDerivation(e.target.value)} disabled={loading} />
-
{senderAddress}
+
{senderLabel}
@@ -107,9 +130,9 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
{
- action === 'transfer'
- ?
- :
+ action === 'transfer'
+ ?
+ :
}
@@ -118,11 +141,18 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
>
)
+
+ function removeUrlParams () {
+ const url = new URL(window.location.href);
+ url.searchParams.delete('transactionHashes');
+ window.history.replaceState({}, document.title, url);
+ }
}
EthereumView.propTypes = {
props: PropTypes.shape({
setStatus: PropTypes.func.isRequired,
MPC_CONTRACT: PropTypes.string.isRequired,
+ transactions: PropTypes.arrayOf(PropTypes.string).isRequired
}).isRequired
};
\ No newline at end of file
diff --git a/src/components/Ethereum/Transfer.jsx b/src/components/Ethereum/Transfer.jsx
index cf1076c..7aa5c9e 100644
--- a/src/components/Ethereum/Transfer.jsx
+++ b/src/components/Ethereum/Transfer.jsx
@@ -5,7 +5,7 @@ import { forwardRef } from "react";
import { useImperativeHandle } from "react";
export const TransferForm = forwardRef(({ props: { Eth, senderAddress, loading } }, ref) => {
- const [receiver, setReceiver] = useState("0x427F9620Be0fe8Db2d840E2b6145D1CF2975bcaD");
+ const [receiver, setReceiver] = useState("0xe0f3B7e68151E9306727104973752A415c2bcbEb");
const [amount, setAmount] = useState(0.005);
useImperativeHandle(ref, () => ({
diff --git a/src/hooks/debounce.jsx b/src/hooks/debounce.jsx
index 5f10712..2ee46e4 100644
--- a/src/hooks/debounce.jsx
+++ b/src/hooks/debounce.jsx
@@ -1,15 +1,14 @@
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState } from "react";
-export function useDebounce(value, delay = 500){
+export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
- const timerRef = useRef();
useEffect(() => {
- timerRef.current = setTimeout(() => setDebouncedValue(value), delay);
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
- return () => {
- clearTimeout(timerRef.current);
- };
+ return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
diff --git a/src/services/ethereum.js b/src/services/ethereum.js
index 6a72e1c..a82099d 100644
--- a/src/services/ethereum.js
+++ b/src/services/ethereum.js
@@ -62,34 +62,45 @@ export class Ethereum {
chain: this.chain_id,
};
- // Return the message hash
+ // Create a transaction
const transaction = FeeMarketEIP1559Transaction.fromTxData(transactionData, { common });
const payload = transaction.getHashedMessageToSign();
+
+ // Store in sessionStorage for later
+ sessionStorage.setItem('transaction', transaction.serialize());
+
return { transaction, payload };
}
- async requestSignatureToMPC(wallet, contractId, path, ethPayload, transaction, sender) {
+ async requestSignatureToMPC(wallet, contractId, path, ethPayload) {
// Ask the MPC to sign the payload
- const payload = Array.from(ethPayload.reverse());
- const [big_r, big_s] = await wallet.callMethod({ contractId, method: 'sign', args: { payload, path, key_version: 0 }, gas: '250000000000000' });
+ sessionStorage.setItem('derivation', path);
- // reconstruct the signature
- const r = Buffer.from(big_r.substring(2), 'hex');
- const s = Buffer.from(big_s, 'hex');
+ const payload = Array.from(ethPayload);
+ const { big_r, s, recovery_id } = await wallet.callMethod({ contractId, method: 'sign', args: { request: { payload, path, key_version: 0 } }, gas: '250000000000000', deposit: '1' });
+ return { big_r, s, recovery_id };
+ }
- const candidates = [0n, 1n].map((v) => transaction.addSignature(v, r, s));
- const signature = candidates.find((c) => c.getSenderAddress().toString().toLowerCase() === sender.toLowerCase());
+ async reconstructSignature(big_r, S, recovery_id, transaction) {
+ // reconstruct the signature
+ const r = Buffer.from(big_r.affine_point.substring(2), 'hex');
+ const s = Buffer.from(S.scalar, 'hex');
+ const v = recovery_id;
- if (!signature) {
- throw new Error("Signature is not valid");
- }
+ const signature = transaction.addSignature(v, r, s);
if (signature.getValidationErrors().length > 0) throw new Error("Transaction validation errors");
if (!signature.verifySignature()) throw new Error("Signature is not valid");
-
return signature;
}
+ async reconstructSignatureFromLocalSession(big_r, s, recovery_id, sender) {
+ const serialized = Uint8Array.from(JSON.parse(`[${sessionStorage.getItem('transaction')}]`));
+ const transaction = FeeMarketEIP1559Transaction.fromSerializedTx(serialized);
+ console.log("transaction", transaction)
+ return this.reconstructSignature(big_r, s, recovery_id, transaction, sender);
+ }
+
// This code can be used to actually relay the transaction to the Ethereum network
async relayTransaction(signedTransaction) {
const serializedTx = bytesToHex(signedTransaction.serialize());
diff --git a/src/services/kdf.js b/src/services/kdf.js
index c744d99..bc7fa82 100644
--- a/src/services/kdf.js
+++ b/src/services/kdf.js
@@ -1,9 +1,8 @@
import { base_decode } from 'near-api-js/lib/utils/serialize';
import { ec as EC } from 'elliptic';
-import BN from 'bn.js';
-import keccak from 'keccak';
-import hash from 'hash.js';
+import { keccak256 } from "viem";import hash from 'hash.js';
import bs58check from 'bs58check';
+import { sha3_256 } from 'js-sha3'
const rootPublicKey = 'secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3';
@@ -12,34 +11,15 @@ export function najPublicKeyStrToUncompressedHexPoint() {
return res;
}
-async function sha256Hash(str) {
- const encoder = new TextEncoder();
- const data = encoder.encode(str);
-
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
-
- const hashArray = [...new Uint8Array(hashBuffer)];
- return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
-}
-
-function sha256StringToScalarLittleEndian(hashString) {
- const littleEndianString = hashString.match(/../g).reverse().join('');
-
- const scalar = new BN(littleEndianString, 16);
-
- return scalar;
-}
-
export async function deriveChildPublicKey(
parentUncompressedPublicKeyHex,
signerId,
path = ''
) {
- const ec = new EC('secp256k1');
- let scalar = await sha256Hash(
+ const ec = new EC("secp256k1");
+ const scalarHex = sha3_256(
`near-mpc-recovery v0.1.0 epsilon derivation:${signerId},${path}`
);
- scalar = sha256StringToScalarLittleEndian(scalar);
const x = parentUncompressedPublicKeyHex.substring(2, 66);
const y = parentUncompressedPublicKeyHex.substring(66);
@@ -48,24 +28,20 @@ export async function deriveChildPublicKey(
const oldPublicKeyPoint = ec.curve.point(x, y);
// Multiply the scalar by the generator point G
- const scalarTimesG = ec.g.mul(scalar);
+ const scalarTimesG = ec.g.mul(scalarHex);
// Add the result to the old public key point
const newPublicKeyPoint = oldPublicKeyPoint.add(scalarTimesG);
-
- return '04' + (
- newPublicKeyPoint.getX().toString('hex').padStart(64, '0') +
- newPublicKeyPoint.getY().toString('hex').padStart(64, '0')
- );
+ const newX = newPublicKeyPoint.getX().toString("hex").padStart(64, "0");
+ const newY = newPublicKeyPoint.getY().toString("hex").padStart(64, "0");
+ return "04" + newX + newY;
}
export function uncompressedHexPointToEvmAddress(uncompressedHexPoint) {
- const address = keccak('keccak256')
- .update(Buffer.from(uncompressedHexPoint.substring(2), 'hex'))
- .digest('hex');
+ const addressHash = keccak256(`0x${uncompressedHexPoint.slice(2)}`);
// Ethereum address is last 20 bytes of hash (40 characters), prefixed with 0x
- return '0x' + address.substring(address.length - 40)
+ return ("0x" + addressHash.substring(addressHash.length - 40));
}
export async function uncompressedHexPointToBtcAddress(publicKeyHex, network) {
diff --git a/src/services/near-wallet.js b/src/services/near-wallet.js
index fdffe21..8a0302b 100644
--- a/src/services/near-wallet.js
+++ b/src/services/near-wallet.js
@@ -8,6 +8,7 @@ import { setupModal } from '@near-wallet-selector/modal-ui';
import { setupWalletSelector } from '@near-wallet-selector/core';
import { setupHereWallet } from '@near-wallet-selector/here-wallet';
import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
+import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet';
const THIRTY_TGAS = '30000000000000';
const NO_DEPOSIT = '0';
@@ -35,7 +36,7 @@ export class Wallet {
startUp = async (accountChangeHook) => {
this.selector = setupWalletSelector({
network: {networkId: this.networkId, nodeUrl: 'https://rpc.testnet.pagoda.co'},
- modules: [setupMyNearWallet(), setupHereWallet()]
+ modules: [setupMyNearWallet(), setupHereWallet(), setupMeteorWallet()]
});
const walletSelector = await this.selector;