diff --git a/package.json b/package.json index f9cb166..5cdc74d 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,15 @@ "@ethereumjs/common": "^4.3.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", + "@near-wallet-selector/bitte-wallet": "^8.9.13", "@near-wallet-selector/core": "^8.9.13", "@near-wallet-selector/here-wallet": "^8.9.13", "@near-wallet-selector/meteor-wallet": "^8.9.13", "@near-wallet-selector/modal-ui": "^8.9.13", "@near-wallet-selector/my-near-wallet": "^8.9.13", - "@near-wallet-selector/bitte-wallet": "^8.9.13", "@vitejs/plugin-react": "^4.2.1", "axios": "^1.6.8", + "bech32": "^2.0.0", "bitcoinjs-lib": "^6.1.5", "bn.js": "^5.2.1", "bs58check": "^3.0.1", diff --git a/src/App.jsx b/src/App.jsx index 254dbc3..74624cf 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,9 +5,7 @@ import Navbar from "./components/Navbar" import { Wallet } from "./services/near-wallet"; import { EthereumView } from "./components/Ethereum/Ethereum"; import { BitcoinView } from "./components/Bitcoin"; - -// CONSTANTS -const MPC_CONTRACT = 'v1.signer-prod.testnet'; +import { MPC_CONTRACT } from './services/kdf/mpc'; // NEAR WALLET const wallet = new Wallet({ network: 'testnet' }); @@ -47,8 +45,8 @@ function App() { - {chain === 'eth' && } - {chain === 'btc' && } + {chain === 'eth' && } + {chain === 'btc' && } } diff --git a/src/components/Bitcoin.jsx b/src/components/Bitcoin.jsx index 72a53f6..8f3b166 100644 --- a/src/components/Bitcoin.jsx +++ b/src/components/Bitcoin.jsx @@ -1,13 +1,13 @@ import { useState, useEffect, useContext } from "react"; import { NearContext } from "../context"; -import { Bitcoin as Bitcoin } from "../services/bitcoin"; import { useDebounce } from "../hooks/debounce"; import PropTypes from 'prop-types'; +import { Bitcoin } from "../services/bitcoin"; -const BTC = Bitcoin; +const BTC = new Bitcoin('testnet'); -export function BitcoinView({ props: { setStatus, MPC_CONTRACT, transactions } }) { +export function BitcoinView({ props: { setStatus, transactions } }) { const { wallet, signedAccountId } = useContext(NearContext); const [receiver, setReceiver] = useState("tb1q86ec0aszet5r3qt02j77f3dvxruk7tuqdlj0d5"); @@ -20,7 +20,7 @@ export function BitcoinView({ props: { setStatus, MPC_CONTRACT, transactions } } const [derivation, setDerivation] = useState("bitcoin-1"); const derivationPath = useDebounce(derivation, 500); - + const getSignedTx = async () => { const signedTx = await wallet.getTransactionResult(transactions[0]) console.log('signedTx', signedTx) @@ -53,9 +53,13 @@ export function BitcoinView({ props: { setStatus, MPC_CONTRACT, transactions } } async function chainSignature() { setStatus('🏗️ Creating transaction'); + + const { psbt, utxos } = await BTC.createTransaction({ from: senderAddress, to: receiver, amount, path: derivationPath, wallet }); + setStatus('🕒 Asking MPC to sign the transaction, this might take a while...'); + try { - const signedTransaction = await BTC.getSignature({ from: senderAddress, publicKey: senderPK, to: receiver, amount, path: derivationPath, wallet }); + const signedTransaction = await BTC.requestSignatureToMPC({ psbt, utxos, publicKey: senderPK, path: derivationPath, wallet }); setStatus('✅ Signed payload ready to be relayed to the Bitcoin network'); setSignedTransaction(signedTransaction); setStep('relay'); @@ -71,10 +75,10 @@ export function BitcoinView({ props: { setStatus, MPC_CONTRACT, transactions } } setStatus('🔗 Relaying transaction to the Bitcoin network... this might take a while'); try { - const txHash = await BTC.broadcast({ from: senderAddress, publicKey: senderPK, to: receiver, amount, path: derivationPath, sig: signedTransaction }); + const txHash = await BTC.broadcastTX(signedTransaction); setStatus( <> - ✅ Successful + ✅ Successfully Broadcasted ); } catch (e) { @@ -125,6 +129,6 @@ export function BitcoinView({ props: { setStatus, MPC_CONTRACT, transactions } } BitcoinView.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/Ethereum.jsx b/src/components/Ethereum/Ethereum.jsx index ba9eddc..d41ea0a 100644 --- a/src/components/Ethereum/Ethereum.jsx +++ b/src/components/Ethereum/Ethereum.jsx @@ -1,17 +1,18 @@ import { useState, useEffect, useContext } from "react"; import { NearContext } from "../../context"; -import { Ethereum } from "../../services/ethereum"; import { useDebounce } from "../../hooks/debounce"; import PropTypes from 'prop-types'; import { useRef } from "react"; import { TransferForm } from "./Transfer"; import { FunctionCallForm } from "./FunctionCall"; +import { Ethereum } from "../../services/ethereum"; +import { MPC_CONTRACT } from "../../services/kdf/mpc"; const Sepolia = 11155111; -const Eth = new Ethereum('https://rpc2.sepolia.org', Sepolia); +const Eth = new Ethereum('https://sepolia.drpc.org', Sepolia); -export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } }) { +export function EthereumView({ props: { setStatus, transactions } }) { const { wallet, signedAccountId } = useContext(NearContext); const [loading, setLoading] = useState(false); @@ -24,7 +25,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } const [derivation, setDerivation] = useState(sessionStorage.getItem('derivation') || "ethereum-1"); const derivationPath = useDebounce(derivation, 1200); - const [reloaded, setReloaded] = useState(transactions.length? true : false); + const [reloaded, setReloaded] = useState(transactions.length ? true : false); const childRef = useRef(); @@ -34,8 +35,8 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } 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); + const signedTransaction = await Eth.reconstructSignedTXFromLocalSession(big_r, s, recovery_id, senderAddress); + setSignedTransaction(signedTransaction); setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`); setStep('relay'); @@ -55,7 +56,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } useEffect(() => { setEthAddress() - console.log(derivationPath) + async function setEthAddress() { const { address } = await Eth.deriveAddress(signedAccountId, derivationPath); setSenderAddress(address); @@ -69,13 +70,15 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } async function chainSignature() { setStatus('🏗️ Creating transaction'); - const { transaction, payload } = await childRef.current.createPayload(); - // const { transaction, payload } = await Eth.createPayload(senderAddress, receiver, amount, undefined); + const { transaction } = await childRef.current.createTransaction(); setStatus(`🕒 Asking ${MPC_CONTRACT} to sign the transaction, this might take a while`); try { - 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); + // to reconstruct on reload + sessionStorage.setItem('derivation', derivationPath); + + const { big_r, s, recovery_id } = await Eth.requestSignatureToMPC({ wallet, path: derivationPath, transaction }); + const signedTransaction = await Eth.reconstructSignedTransaction(big_r, s, recovery_id, transaction); setSignedTransaction(signedTransaction); setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`); @@ -89,9 +92,8 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } async function relayTransaction() { setLoading(true); setStatus('🔗 Relaying transaction to the Ethereum network... this might take a while'); - try { - const txHash = await Eth.relayTransaction(signedTransaction); + const txHash = await Eth.broadcastTX(signedTransaction); setStatus( <> ✅ Successful @@ -142,7 +144,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } ) - function removeUrlParams () { + function removeUrlParams() { const url = new URL(window.location.href); url.searchParams.delete('transactionHashes'); window.history.replaceState({}, document.title, url); @@ -152,7 +154,6 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } 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/FunctionCall.jsx b/src/components/Ethereum/FunctionCall.jsx index a4851a5..385ed5a 100644 --- a/src/components/Ethereum/FunctionCall.jsx +++ b/src/components/Ethereum/FunctionCall.jsx @@ -60,15 +60,13 @@ export const FunctionCallForm = forwardRef(({ props: { Eth, senderAddress, loadi useEffect(() => { getNumber() }, []); useImperativeHandle(ref, () => ({ - async createPayload() { + async createTransaction() { const data = Eth.createTransactionData(contract, abi, 'set', [number]); - const { transaction, payload } = await Eth.createPayload(senderAddress, contract, 0, data); - return { transaction, payload }; + const { transaction } = await Eth.createTransaction({ sender: senderAddress, receiver: contract, amount: 0, data }); + return { transaction }; }, - async afterRelay() { - getNumber(); - } + async afterRelay() { getNumber(); } })); return ( @@ -110,7 +108,7 @@ FunctionCallForm.propTypes = { senderAddress: PropTypes.string.isRequired, loading: PropTypes.bool.isRequired, Eth: PropTypes.shape({ - createPayload: PropTypes.func.isRequired, + createTransaction: PropTypes.func.isRequired, createTransactionData: PropTypes.func.isRequired, getContractViewFunction: PropTypes.func.isRequired, }).isRequired, diff --git a/src/components/Ethereum/Transfer.jsx b/src/components/Ethereum/Transfer.jsx index 7aa5c9e..c20e14f 100644 --- a/src/components/Ethereum/Transfer.jsx +++ b/src/components/Ethereum/Transfer.jsx @@ -5,13 +5,13 @@ import { forwardRef } from "react"; import { useImperativeHandle } from "react"; export const TransferForm = forwardRef(({ props: { Eth, senderAddress, loading } }, ref) => { - const [receiver, setReceiver] = useState("0xe0f3B7e68151E9306727104973752A415c2bcbEb"); + const [receiver, setReceiver] = useState("0xb8A6a4eb89b27703E90ED18fDa1101c7aa02930D"); const [amount, setAmount] = useState(0.005); useImperativeHandle(ref, () => ({ - async createPayload() { - const { transaction, payload } = await Eth.createPayload(senderAddress, receiver, amount, undefined); - return { transaction, payload }; + async createTransaction() { + const { transaction } = await Eth.createTransaction({ sender: senderAddress, receiver, amount }); + return { transaction }; }, async afterRelay() { } })); @@ -40,7 +40,7 @@ TransferForm.propTypes = { senderAddress: PropTypes.string.isRequired, loading: PropTypes.bool.isRequired, Eth: PropTypes.shape({ - createPayload: PropTypes.func.isRequired + createTransaction: PropTypes.func.isRequired }).isRequired }).isRequired }; diff --git a/src/services/bitcoin.js b/src/services/bitcoin.js index a83fe3d..be4e404 100644 --- a/src/services/bitcoin.js +++ b/src/services/bitcoin.js @@ -1,28 +1,155 @@ import * as ethers from 'ethers'; -import * as bitcoin from "bitcoinjs-lib"; import { fetchJson } from './utils'; -import { sign } from './near'; import * as bitcoinJs from 'bitcoinjs-lib'; -import secp256k1 from 'secp256k1'; -import { generateBtcAddress, rootPublicKey } from './btcKdf'; +import { generateBtcAddress } from './kdf/btc'; +import { MPC_CONTRACT } from './kdf/mpc'; -const constructPsbt = async ( +export class Bitcoin { + name = 'Bitcoin'; + currency = 'sats'; + + constructor(networkId) { + this.networkId = networkId; + this.name = `Bitcoin ${networkId === 'testnet' ? 'Testnet' : 'Mainnet'}`; + this.explorer = `https://blockstream.info/${networkId === 'testnet' ? 'testnet' : ''}`; + } + + deriveAddress = async (accountId, derivation_path) => { + const { address, publicKey } = await generateBtcAddress({ + accountId, + path: derivation_path, + isTestnet: true, + addressType: 'segwit' + }); + return { address, publicKey }; + } + + getUtxos = async ({ address }) => { + const bitcoinRpc = `https://blockstream.info/${this.networkId === 'testnet' ? 'testnet' : ''}/api`; + try { + const utxos = await fetchJson(`${bitcoinRpc}/address/${address}/utxo`); + return utxos; + } catch (e) { console.log('e', e) } + } + + getBalance = async ({ address }) => { + const utxos = await this.getUtxos({ address }); + let balance = utxos.reduce((acc, utxo) => acc + utxo.value, 0); + return balance; + } + + createTransaction = async ({ from: address, to, amount }) => { + let utxos = await this.getUtxos({ address }); + if (!utxos) return + + // Use the utxo with the highest value + utxos.sort((a, b) => b.value - a.value); + utxos = [utxos[0]]; + + const psbt = await constructPsbt(address, utxos, to, amount, this.networkId) + if (!psbt) return + + return { utxos, psbt }; + } + + requestSignatureToMPC = async ({ + wallet, + path, + psbt, + utxos, + publicKey, + attachedDeposit = 1, + }) => { + const keyPair = { + publicKey: Buffer.from(publicKey, 'hex'), + sign: async (transactionHash) => { + const utxo = utxos[0]; // The UTXO being spent + const value = utxo.value; // The value in satoshis of the UTXO being spent + + if (isNaN(value)) { + throw new Error(`Invalid value for UTXO at index ${transactionHash}: ${utxo.value}`); + } + + const payload = Object.values(ethers.getBytes(transactionHash)); + + // Sign the payload using MPC + const args = { request: { payload, path, key_version: 0, } }; + + const { big_r, s } = await wallet.callMethod({ + contractId: MPC_CONTRACT, + method: 'sign', + args, + gas: '250000000000000', // 250 Tgas + deposit: attachedDeposit, + }); + + // Reconstruct the signature + const rHex = big_r.affine_point.slice(2); // Remove the "03" prefix + let sHex = s.scalar; + + // Pad s if necessary + if (sHex.length < 64) { + sHex = sHex.padStart(64, '0'); + } + + const rBuf = Buffer.from(rHex, 'hex'); + const sBuf = Buffer.from(sHex, 'hex'); + + // Combine r and s + return Buffer.concat([rBuf, sBuf]); + }, + }; + + // Sign each input manually + await Promise.all( + utxos.map(async (_, index) => { + try { + await psbt.signInputAsync(index, keyPair); + console.log(`Input ${index} signed successfully`); + } catch (e) { + console.warn(`Error signing input ${index}:`, e); + } + }) + ); + + psbt.finalizeAllInputs(); // Finalize the PSBT + + return psbt; // Return the generated signature + } + + broadcastTX = async (signedTransaction) => { + // broadcast tx + const bitcoinRpc = `https://blockstream.info/${this.networkId === 'testnet' ? 'testnet' : ''}/api`; + const res = await fetch(`https://corsproxy.io/?${bitcoinRpc}/tx`, { + method: 'POST', + body: signedTransaction.extractTransaction().toHex(), + }); + if (res.status === 200) { + const hash = await res.text(); + return hash + } else { + throw Error(res); + } + } +} + +async function getFeeRate(networkId, blocks = 6) { + const bitcoinRpc = `https://blockstream.info/${networkId === 'testnet' ? 'testnet' : ''}/api`; + const rate = await fetchJson(`${bitcoinRpc}/fee-estimates`); + return rate[blocks].toFixed(0); +} + +async function constructPsbt( address, + utxos, to, - amount -) => { - const networkId = 'testnet' - const bitcoinRpc = `https://blockstream.info/${networkId === 'testnet' ? 'testnet' : ''}/api`; + amount, + networkId, +) { if (!address) return console.log('must provide a sending address'); - - const { getBalance, explorer } = Bitcoin; const sats = parseInt(amount); - // Get UTXOs - const utxos = await getBalance({ address, getUtxos: true }); - if (!utxos || utxos.length === 0) throw new Error('No utxos detected for address: ', address); - // Check balance (TODO include fee in check) if (utxos[0].value < sats) { return console.log('insufficient funds'); @@ -31,12 +158,12 @@ const constructPsbt = async ( const psbt = new bitcoinJs.Psbt({ network: networkId === 'testnet' ? bitcoinJs.networks.testnet : bitcoinJs.networks.bitcoin }); let totalInput = 0; - + await Promise.all( utxos.map(async (utxo) => { totalInput += utxo.value; - const transaction = await fetchTransaction(utxo.txid); + const transaction = await fetchTransaction(networkId, utxo.txid); let inputOptions; const scriptHex = transaction.outs[utxo.vout].script.toString('hex'); @@ -86,7 +213,7 @@ const constructPsbt = async ( psbt.addInput(inputOptions); }) ); - + // Add output to the recipient psbt.addOutput({ address: to, @@ -94,9 +221,9 @@ const constructPsbt = async ( }); // Calculate fee (replace with real fee estimation) - const feeRate = await fetchJson(`${bitcoinRpc}/fee-estimates`); + const feeRate = await getFeeRate(networkId); const estimatedSize = utxos.length * 148 + 2 * 34 + 10; - const fee = estimatedSize * (feeRate[6] + 3); + const fee = (estimatedSize * feeRate).toFixed(0); const change = totalInput - sats - fee; // Add change output if necessary @@ -107,221 +234,10 @@ const constructPsbt = async ( }); } - // Return the constructed PSBT and UTXOs for signing - return [utxos, psbt, explorer]; + return psbt; }; -export const Bitcoin = { - name: 'Bitcoin Testnet', - currency: 'sats', - explorer: 'https://blockstream.info/testnet', - deriveAddress: async (accountId, derivation_path) => { - const { address, publicKey } = await generateBtcAddress({ - publicKey: rootPublicKey, - accountId, - path: derivation_path, - isTestnet: true, - addressType: 'segwit' - }); - return { address, publicKey }; - }, - getBalance: async ({ address, getUtxos = false }) => { - const networkId = 'testnet' - try { - const res = await fetchJson( - `https://blockstream.info${networkId === 'testnet' ? '/testnet': ''}/api/address/${address}/utxo`, - ); - - if (!res) return - - let utxos = res.map((utxo) => ({ - txid: utxo.txid, - vout: utxo.vout, - value: utxo.value, - })); - - console.log('utxos', utxos) - let maxValue = 0; - utxos.forEach((utxo) => { - if (utxo.value > maxValue) maxValue = utxo.value; - }); - utxos = utxos.filter((utxo) => utxo.value === maxValue); - - if (!utxos || !utxos.length) { - console.log( - 'no utxos for address', - address, - 'please fund address and try again', - ); - } - - return getUtxos ? utxos : maxValue; - } catch (e) { - console.log('e', e) - } - }, - getAndBroadcastSignature: async ({ - from: address, - publicKey, - to, - amount, - path, - }) => { - console.log('About to call getSignature...'); - const sig = await bitcoin.getSignature({ - from: address, - publicKey, - to, - amount, - path, - }); - - // Check if the signature was successfully generated - if (!sig) { - console.error('Failed to generate signature'); - return; - } - - const broadcastResult = await bitcoin.broadcast({ - from: address, - publicKey: publicKey, - to, - amount, - path, - sig - }); - - return broadcastResult - }, - getSignature: async ({ - from: address, - publicKey, - to, - amount, - path, - wallet - }) => { - const result = await constructPsbt(address, to, amount) - if (!result) return - const [utxos, psbt] = result; - - let signature - const keyPair = { - publicKey: Buffer.from(publicKey, 'hex'), - sign: async (transactionHash) => { - const utxo = utxos[0]; // The UTXO being spent - const value = utxo.value; // The value in satoshis of the UTXO being spent - - if (isNaN(value)) { - throw new Error(`Invalid value for UTXO at index ${transactionHash}: ${utxo.value}`); - } - - const payload = Object.values(ethers.getBytes(transactionHash)); - - // Sign the payload using the external `sign` method (e.g., NEAR signature) - signature = await sign(payload, path, wallet); - }, - }; - - try { - // Sign each input manually - await Promise.all( - utxos.map(async (_, index) => { - try { - await psbt.signInputAsync(index, keyPair); - console.log(`Input ${index} signed successfully`); - } catch (e) { - console.warn(`Error signing input ${index}:`, e); - } - }) - ); - } catch (e) { - console.error('Error signing inputs:', e); - } - - return signature; // Return the generated signature - }, - broadcast: async ({ - from: address, - publicKey, - to, - amount, - sig - }) => { - const result = await constructPsbt(address, to, amount) - if (!result) return - const [utxos, psbt, explorer] = result; - - const keyPair = { - publicKey: Buffer.from(publicKey, 'hex'), - sign: () => { - const rHex = sig.big_r.affine_point.slice(2); // Remove the "03" prefix - let sHex = sig.s.scalar; - - // Pad s if necessary - if (sHex.length < 64) { - sHex = sHex.padStart(64, '0'); - } - - const rBuf = Buffer.from(rHex, 'hex'); - const sBuf = Buffer.from(sHex, 'hex'); - - // Combine r and s - const rawSignature = Buffer.concat([rBuf, sBuf]); - - return rawSignature; - }, - }; - - await Promise.all( - utxos.map(async (_, index) => { - console.log('utxo:', _) - try { - await psbt.signInputAsync(index, keyPair); - } catch (e) { - console.warn(e, 'not signed'); - } - }), - ); - - try { - psbt.finalizeAllInputs(); - } catch (e) { - console.log('e', e) - } - - // const networkId = useStore.getState().networkId - const networkId = 'testnet' - const bitcoinRpc = `https://blockstream.info/${networkId === 'testnet' ? 'testnet' : ''}/api`; - - console.log('psbt', psbt.extractTransaction().toHex()) - // broadcast tx - try { - const res = await fetch(`https://corsproxy.io/?${bitcoinRpc}/tx`, { - method: 'POST', - body: psbt.extractTransaction().toHex(), - }); - if (res.status === 200) { - const hash = await res.text(); - console.log('tx hash', hash); - console.log('explorer link', `${explorer}/tx/${hash}`); - console.log( - 'NOTE: it might take a minute for transaction to be included in mempool', - ); - - return hash - } else { - return res - } - } catch (e) { - console.log('error broadcasting bitcoin tx', JSON.stringify(e)); - } - return 'failed' - }, -}; - -async function fetchTransaction(transactionId) { - const networkId = 'testnet' +async function fetchTransaction(networkId, transactionId) { const bitcoinRpc = `https://blockstream.info/${networkId === 'testnet' ? 'testnet' : ''}/api`; const data = await fetchJson(`${bitcoinRpc}/tx/${transactionId}`); @@ -355,22 +271,4 @@ async function fetchTransaction(transactionId) { }); return tx; -} - -export const recoverPubkeyFromSignature = (transactionHash, rawSignature) => { - let pubkeys = []; - [0,1].forEach(num => { - const recoveredPubkey = secp256k1.recover( - transactionHash, // 32 byte hash of message - rawSignature, // 64 byte signature of message (not DER, 32 byte R and 32 byte S with 0x00 padding) - num, // number 1 or 0. This will usually be encoded in the base64 message signature - false, // true if you want result to be compressed (33 bytes), false if you want it uncompressed (65 bytes) this also is usually encoded in the base64 signature - ); - console.log('recoveredPubkey', recoveredPubkey) - const buffer = Buffer.from(recoveredPubkey); - // Convert the Buffer to a hexadecimal string - const hexString = buffer.toString('hex'); - pubkeys.push(hexString) - }) - return pubkeys -} +} \ No newline at end of file diff --git a/src/services/ethereum.js b/src/services/ethereum.js index 6585a6e..08f0cbe 100644 --- a/src/services/ethereum.js +++ b/src/services/ethereum.js @@ -1,28 +1,29 @@ import { Web3 } from "web3" import { bytesToHex } from '@ethereumjs/util'; import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx'; -import { deriveChildPublicKey, najPublicKeyStrToUncompressedHexPoint, uncompressedHexPointToEvmAddress } from '../services/kdf'; +import { generateEthAddress } from './kdf/eth'; import { Common } from '@ethereumjs/common' import { Contract, JsonRpcProvider } from "ethers"; -import { parseNearAmount } from "near-api-js/lib/utils/format"; +import { MPC_CONTRACT } from "./kdf/mpc"; export class Ethereum { constructor(chain_rpc, chain_id) { this.web3 = new Web3(chain_rpc); + window.web3 = this.web3; this.provider = new JsonRpcProvider(chain_rpc); this.chain_id = chain_id; this.queryGasPrice(); } async deriveAddress(accountId, derivation_path) { - const publicKey = await deriveChildPublicKey(najPublicKeyStrToUncompressedHexPoint(), accountId, derivation_path); - const address = await uncompressedHexPointToEvmAddress(publicKey); - return { publicKey: Buffer.from(publicKey, 'hex'), address }; + const { address, publicKey } = await generateEthAddress({ accountId, derivation_path }); + return { address, publicKey }; } async queryGasPrice() { - const maxFeePerGas = await this.web3.eth.getGasPrice(); + const block = await this.web3.eth.getBlock("latest"); const maxPriorityFeePerGas = await this.web3.eth.getMaxPriorityFeePerGas(); + const maxFeePerGas = block.baseFeePerGas * 2n + maxPriorityFeePerGas; return { maxFeePerGas, maxPriorityFeePerGas }; } @@ -43,7 +44,7 @@ export class Ethereum { return contract.interface.encodeFunctionData(methodName, args); } - async createPayload(sender, receiver, amount, data) { + async createTransaction({ sender, receiver, amount, data = undefined }) { const common = new Common({ chain: this.chain_id }); // Get the nonce & gas price @@ -64,47 +65,52 @@ export class Ethereum { // 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 }; + return { transaction }; } - async requestSignatureToMPC(wallet, contractId, path, ethPayload) { - // Ask the MPC to sign the payload - sessionStorage.setItem('derivation', path); + async requestSignatureToMPC({ wallet, path, transaction, attachedDeposit = 1 }) { + const payload = Array.from(transaction.getHashedMessageToSign()); + + const { big_r, s, recovery_id } = await wallet.callMethod({ + contractId: MPC_CONTRACT, + method: 'sign', + args: { request: { payload, path, key_version: 0 } }, + gas: '250000000000000', // 250 Tgas + deposit: attachedDeposit, + }); - 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: parseNearAmount('0.25') }); return { big_r, s, recovery_id }; } - async reconstructSignature(big_r, S, recovery_id, transaction) { + async reconstructSignedTransaction(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; - const signature = transaction.addSignature(v, r, s); + const signedTx = 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; + if (signedTx.getValidationErrors().length > 0) throw new Error("Transaction validation errors"); + if (!signedTx.verifySignature()) throw new Error("Signature is not valid"); + return signedTx; } - async reconstructSignatureFromLocalSession(big_r, s, recovery_id, sender) { + async reconstructSignedTXFromLocalSession(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); + return this.reconstructSignedTransaction(big_r, s, recovery_id, transaction, sender); } // This code can be used to actually relay the transaction to the Ethereum network - async relayTransaction(signedTransaction) { + async broadcastTX(signedTransaction) { const serializedTx = bytesToHex(signedTransaction.serialize()); - const relayed = await this.web3.eth.sendSignedTransaction(serializedTx); - return relayed.transactionHash + const relayed = this.web3.eth.sendSignedTransaction(serializedTx); + let txHash; + await relayed.on('transactionHash', (hash) => { txHash = hash }); + return txHash; } } \ No newline at end of file diff --git a/src/services/btcKdf.js b/src/services/kdf/btc.js similarity index 93% rename from src/services/btcKdf.js rename to src/services/kdf/btc.js index dd7fc66..c375383 100644 --- a/src/services/btcKdf.js +++ b/src/services/kdf/btc.js @@ -4,16 +4,10 @@ import { sha3_256 } from 'js-sha3'; import hash from 'hash.js'; import bs58check from 'bs58check'; import { bech32 } from 'bech32' +import { MPC_KEY } from './mpc'; export const rootPublicKey = 'secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3'; -export function najPublicKeyStrToUncompressedHexPoint( - najPublicKeyStr -) { - const decodedKey = base_decode(najPublicKeyStr.split(':')[1]); - return '04' + Buffer.from(decodedKey).toString('hex'); -} - export function najPublicKeyStrToCompressedPoint(najPublicKeyStr) { const ec = new EC('secp256k1'); @@ -94,14 +88,13 @@ export async function uncompressedHexPointToBtcAddress( } export async function generateBtcAddress({ - publicKey, accountId, path = '', isTestnet = true, addressType = 'segwit' }) { const childPublicKey = await deriveChildPublicKey( - najPublicKeyStrToCompressedPoint(publicKey), // Use the compressed key + najPublicKeyStrToCompressedPoint(MPC_KEY), // Use the compressed key accountId, path ); diff --git a/src/services/kdf.js b/src/services/kdf/eth.js similarity index 56% rename from src/services/kdf.js rename to src/services/kdf/eth.js index bc7fa82..3f66057 100644 --- a/src/services/kdf.js +++ b/src/services/kdf/eth.js @@ -1,13 +1,17 @@ import { base_decode } from 'near-api-js/lib/utils/serialize'; import { ec as EC } from 'elliptic'; import { keccak256 } from "viem";import hash from 'hash.js'; -import bs58check from 'bs58check'; import { sha3_256 } from 'js-sha3' +import { MPC_KEY } from './mpc'; -const rootPublicKey = 'secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3'; +export async function generateEthAddress({ accountId, derivation_path }) { + const publicKey = await deriveChildPublicKey(najPublicKeyStrToUncompressedHexPoint(), accountId, derivation_path); + const address = await uncompressedHexPointToEvmAddress(publicKey); + return { publicKey: Buffer.from(publicKey, 'hex'), address }; +} export function najPublicKeyStrToUncompressedHexPoint() { - const res = '04' + Buffer.from(base_decode(rootPublicKey.split(':')[1])).toString('hex'); + const res = '04' + Buffer.from(base_decode(MPC_KEY.split(':')[1])).toString('hex'); return res; } @@ -42,33 +46,4 @@ export function uncompressedHexPointToEvmAddress(uncompressedHexPoint) { // Ethereum address is last 20 bytes of hash (40 characters), prefixed with 0x return ("0x" + addressHash.substring(addressHash.length - 40)); -} - -export async function uncompressedHexPointToBtcAddress(publicKeyHex, network) { - // Step 1: SHA-256 hashing of the public key - const publicKeyBytes = Uint8Array.from(Buffer.from(publicKeyHex, 'hex')); - - const sha256HashOutput = await crypto.subtle.digest( - 'SHA-256', - publicKeyBytes - ); - - // Step 2: RIPEMD-160 hashing on the result of SHA-256 - const ripemd160 = hash - .ripemd160() - .update(Buffer.from(sha256HashOutput)) - .digest(); - - // Step 3: Adding network byte (0x00 for Bitcoin Mainnet) - const network_byte = network === 'bitcoin' ? 0x00 : 0x6f; - const networkByte = Buffer.from([network_byte]); - const networkByteAndRipemd160 = Buffer.concat([ - networkByte, - Buffer.from(ripemd160) - ]); - - // Step 4: Base58Check encoding - const address = bs58check.encode(networkByteAndRipemd160); - - return address; } \ No newline at end of file diff --git a/src/services/kdf/mpc.js b/src/services/kdf/mpc.js new file mode 100644 index 0000000..4e46038 --- /dev/null +++ b/src/services/kdf/mpc.js @@ -0,0 +1,2 @@ +export const MPC_CONTRACT = 'v1.signer-prod.testnet' +export const MPC_KEY = 'secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3'; \ No newline at end of file diff --git a/src/services/near-wallet.js b/src/services/near-wallet.js index 09336a1..4697d15 100644 --- a/src/services/near-wallet.js +++ b/src/services/near-wallet.js @@ -13,6 +13,7 @@ import { setupBitteWallet } from '@near-wallet-selector/bitte-wallet'; const THIRTY_TGAS = '30000000000000'; const NO_DEPOSIT = '0'; +const ONE_YOCTO = '1'; export class Wallet { /** @@ -36,7 +37,7 @@ export class Wallet { */ startUp = async (accountChangeHook) => { this.selector = setupWalletSelector({ - network: {networkId: this.networkId, nodeUrl: 'https://rpc.testnet.pagoda.co'}, + network: { networkId: this.networkId, nodeUrl: 'https://rpc.testnet.pagoda.co' }, modules: [setupMyNearWallet(), setupHereWallet(), setupMeteorWallet(), setupBitteWallet()] }); diff --git a/src/services/near.js b/src/services/near.js deleted file mode 100644 index 8ea21bb..0000000 --- a/src/services/near.js +++ /dev/null @@ -1,51 +0,0 @@ -import { providers } from 'near-api-js'; -import { retryWithDelay } from './utils' - -let isSigning = false; - -export async function sign(payload, path, wallet) { - if (isSigning) { - console.warn('Sign function is already running.'); - return; - } - isSigning = true; - - const contractId = 'v1.signer-prod.testnet' - if (!wallet) { - console.error('Wallet is not initialized'); - return; - } - - const args = { - request: { - payload, - path, - key_version: 0, - }, - }; - const attachedDeposit = '500000000000000000000000' - - const result = await wallet.callMethod({ - contractId, - method: 'sign', - args, - gas: '250000000000000', // 250 Tgas - deposit: attachedDeposit, - }); - - return result -} - -// Updated getTransactionResult function using retryWithDelay -export const getTransactionResult = async (txHash) => { - const provider = new providers.JsonRpcProvider({ url: 'http://rpc.mainnet.near.org' }); - - // Define the function to retrieve the transaction result - const fetchTransactionResult = async () => { - const transaction = await provider.txStatus(txHash, 'unnused'); - return providers.getTransactionLastResult(transaction); - }; - - // Use the retry helper to attempt fetching the transaction result - return await retryWithDelay(fetchTransactionResult); -};