diff --git a/examples/send-eth.ts b/examples/send-eth.ts index 372eb23..3077161 100644 --- a/examples/send-eth.ts +++ b/examples/send-eth.ts @@ -1,16 +1,16 @@ import dotenv from "dotenv"; -import { SEPOLIA_CHAIN_ID, setupNearEthAdapter } from "./setup"; +import { setupNearEthAdapter } from "./setup"; dotenv.config(); const run = async (): Promise => { const evm = await setupNearEthAdapter(); - await evm.signAndSendTransaction({ - // Sending to self. - to: evm.address, - // THIS IS ONE WEI! - value: 1n, - chainId: SEPOLIA_CHAIN_ID, - }); + console.log(evm.address); + // throw new Error("You foked up"); + const transactions = [ + { to: evm.address, value: 1n, chainId: 97 }, + { to: evm.address, value: 1n, chainId: 1301 }, + ]; + await evm.signAndSendTransaction(transactions); }; run(); diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts index bfca8ae..66795c9 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/ethereum.ts @@ -32,6 +32,7 @@ export class NearEthAdapter { readonly mpcContract: IMpcContract; readonly address: Address; readonly derivationPath: string; + readonly keyVersion: number; readonly beta: Beta; private constructor(config: { @@ -41,6 +42,7 @@ export class NearEthAdapter { }) { this.mpcContract = config.mpcContract; this.derivationPath = config.derivationPath; + this.keyVersion = 0; this.address = config.sender; this.beta = new Beta(this); } @@ -103,16 +105,29 @@ export class NearEthAdapter { * Note that the signature request is a recursive function. */ async signAndSendTransaction( - txData: BaseTx, + txData: BaseTx[], nearGas?: bigint - ): Promise { - const { transaction, signArgs } = await this.createTxPayload(txData); + ): Promise { + // Note that chainIds must be all different or + // we have to make special consideration for the nonces. + const txArray = await Promise.all( + txData.map((tx) => this.createTxPayload(tx)) + ); + console.log(`Requesting signature from ${this.mpcContract.accountId()}`); - const signature = await this.mpcContract.requestSignature( - signArgs, + const signatures = await this.mpcContract.requestMulti( + txArray.map((tx) => tx.signArgs), nearGas ); - return broadcastSignedTransaction({ transaction, signature }); + const transactions = txArray.map((tx) => tx.transaction); + return Promise.all( + transactions.map((transaction, i) => + broadcastSignedTransaction({ + transaction, + signature: signatures[i]!, + }) + ) + ); } /** diff --git a/src/mpcContract.ts b/src/mpcContract.ts index b264c54..501f659 100644 --- a/src/mpcContract.ts +++ b/src/mpcContract.ts @@ -8,6 +8,7 @@ import { import { TGAS } from "./chains/near"; import { MPCSignature, FunctionCallTransaction, SignArgs } from "./types"; import { transformSignature } from "./utils/signature"; +import { Action, FunctionCall } from "near-api-js/lib/transaction"; /** * Near Contract Type for change methods. @@ -113,6 +114,34 @@ export class MpcContract implements IMpcContract { return transformSignature(mpcSig); }; + requestMulti = async ( + signArgs: SignArgs[], + gas?: bigint + ): Promise => { + const transaction = await this.encodeMulti(signArgs, gas); + // TODO: This is a hack to prevent out of gas errors + const maxGasPerAction = 300000000000000n / BigInt(signArgs.length); + const result = await this.connectedAccount.signAndSendTransaction({ + receiverId: transaction.receiverId, + actions: transaction.actions.map((action) => { + return new Action({ + functionCall: new FunctionCall({ + methodName: "sign", + args: Buffer.from(JSON.stringify(action.params.args)), + gas: maxGasPerAction, + deposit: BigInt(action.params.deposit), + }), + }); + }), + }); + + // Extract signatures from each receipt outcome in order + const mpcSigs = result.receipts_outcome.map( + (receipt) => receipt.outcome.status as MPCSignature + ); + return mpcSigs.map(transformSignature); + }; + async encodeSignatureRequestTx( signArgs: SignArgs, gas?: bigint @@ -133,6 +162,30 @@ export class MpcContract implements IMpcContract { ], }; } + + async encodeMulti( + signArgs: SignArgs[], + gas?: bigint + ): Promise> { + const deposit = await this.getDeposit(); + return { + signerId: this.connectedAccount.accountId, + receiverId: this.contract.contractId, + actions: signArgs.map((args) => { + return { + type: "FunctionCall", + params: { + methodName: "sign", + args: { + request: args, + }, + gas: gasOrDefault(gas), + deposit, + }, + }; + }), + }; + } } function gasOrDefault(gas?: bigint): string { @@ -149,8 +202,13 @@ export interface IMpcContract { deriveEthAddress(derivationPath: string): Promise
; getDeposit(): Promise; requestSignature(signArgs: SignArgs, gas?: bigint): Promise; + requestMulti(signArgs: SignArgs[], gas?: bigint): Promise; encodeSignatureRequestTx( signArgs: SignArgs, gas?: bigint ): Promise>; + encodeMulti( + signArgs: SignArgs[], + gas?: bigint + ): Promise>; } diff --git a/src/utils/mock-sign.ts b/src/utils/mock-sign.ts index 5ce06e9..e52ec4b 100644 --- a/src/utils/mock-sign.ts +++ b/src/utils/mock-sign.ts @@ -48,6 +48,15 @@ export class MockMpcContract implements IMpcContract { "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ); } + requestMulti(_signArgs: SignArgs[], _gas?: bigint): Promise { + throw new Error("Method not implemented."); + } + encodeMulti( + _signArgs: SignArgs[], + _gas?: bigint + ): Promise> { + throw new Error("Method not implemented."); + } accountId(): string { return "mock-mpc.offline"; diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 49f9dad..64a102c 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -27,24 +27,28 @@ describe("End To End", () => { it.skip("signAndSendTransaction", async () => { await expect( - realAdapter.signAndSendTransaction({ - // Sending 1 WEI to self (so we never run out of funds) - to: realAdapter.address, - value: ONE_WEI, - chainId, - }) + realAdapter.signAndSendTransaction([ + { + // Sending 1 WEI to self (so we never run out of funds) + to: realAdapter.address, + value: ONE_WEI, + chainId, + }, + ]) ).resolves.not.toThrow(); }); it.skip("signAndSendTransaction - Gnosis Chain", async () => { await expect( - realAdapter.signAndSendTransaction({ - // Sending 1 WEI to self (so we ~never run out of funds) - to: realAdapter.address, - value: ONE_WEI, - // Gnosis Chain! - chainId: 100, - }) + realAdapter.signAndSendTransaction([ + { + // Sending 1 WEI to self (so we ~never run out of funds) + to: realAdapter.address, + value: ONE_WEI, + // Gnosis Chain! + chainId: 100, + }, + ]) ).resolves.not.toThrow(); }); @@ -54,11 +58,13 @@ describe("End To End", () => { address: mockedAdapter.address, }); await expect( - realAdapter.signAndSendTransaction({ - to, - value: senderBalance + ONE_WEI, - chainId, - }) + realAdapter.signAndSendTransaction([ + { + to, + value: senderBalance + ONE_WEI, + chainId, + }, + ]) ).rejects.toThrow(); });