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);
-};