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;