UNPKG

near-ca-test

Version:

An SDK for controlling Ethereum Accounts from a Near Account.

111 lines (110 loc) 4.69 kB
import { Contract, transactions } from "near-api-js"; import { deriveChildPublicKey, najPublicKeyStrToUncompressedHexPoint, uncompressedHexPointToEvmAddress, } from "./utils/kdf"; import { TGAS } from "./chains/near"; import { signatureFromOutcome } from "./utils/signature"; /** * High-level interface for the Near MPC-Recovery Contract * located in: https://github.com/near/mpc-recovery */ export class MpcContract { rootPublicKey; contract; connectedAccount; constructor(account, contractId, rootPublicKey) { this.connectedAccount = account; this.rootPublicKey = rootPublicKey; this.contract = new Contract(account.connection, contractId, { changeMethods: ["sign"], viewMethods: [ "public_key", "experimental_signature_deposit", "experimantal_signature_deposit", ], useLocalViewExecution: false, }); } accountId() { return this.contract.contractId; } deriveEthAddress = async (derivationPath) => { if (!this.rootPublicKey) { this.rootPublicKey = await this.contract.public_key(); } const publicKey = deriveChildPublicKey(najPublicKeyStrToUncompressedHexPoint(this.rootPublicKey), this.connectedAccount.accountId, derivationPath); return uncompressedHexPointToEvmAddress(publicKey); }; getDeposit = async () => { let deposit = 1e23; try { deposit = await this.contract.experimental_signature_deposit(); } catch (error) { if (error instanceof Error && error.message === "Contract method is not found") { // Clown town deposit = await this.contract.experimantal_signature_deposit(); } else { console.warn(`Failed to get deposit with ${error} - using fallback of 0.1 Near`); } } return BigInt(deposit.toLocaleString("fullwide", { useGrouping: false })).toString(); }; requestSignature = async (signArgs, gas) => { // near-api-js SUX so bad we can't configure this RPC timeout. // const mpcSig = await this.contract.sign({ // signerAccount: this.connectedAccount, // args: { request: signArgs }, // gas: gasOrDefault(gas), // amount: await this.getDeposit(), // }); const transaction = await this.encodeSignatureRequestTx(signArgs, gas); const outcome = await this.signAndSendSignRequest(transaction); return signatureFromOutcome(outcome); }; async encodeSignatureRequestTx(signArgs, gas) { return { signerId: this.connectedAccount.accountId, receiverId: this.contract.contractId, actions: [ { type: "FunctionCall", params: { methodName: "sign", args: { request: signArgs }, gas: gasOrDefault(gas), deposit: await this.getDeposit(), }, }, ], }; } async signAndSendSignRequest(transaction, blockTimeout = 30) { const account = this.connectedAccount; // @ts-expect-error: Account.signTransaction is protected (for no apparantly good reason) const [txHash, signedTx] = await account.signTransaction(this.contract.contractId, transaction.actions.map(({ params: { args, gas, deposit } }) => transactions.functionCall("sign", args, BigInt(gas), BigInt(deposit)))); const provider = account.connection.provider; let outcome = await provider.sendTransactionAsync(signedTx); let pings = 0; while (outcome.final_execution_status != "EXECUTED" && pings < blockTimeout) { // Sleep 1 second before next ping. await new Promise((resolve) => setTimeout(resolve, 1000)); // txStatus times out when waiting for 'EXECUTED'. // Instead we wait for an earlier status type, sleep between and keep pinging. outcome = await provider.txStatus(txHash, account.accountId, "INCLUDED"); pings += 1; } if (pings >= blockTimeout) { console.warn(`Request status polling exited before desired outcome.\n Current status: ${outcome.final_execution_status}\nSignature Request will likley fail.`); } return outcome; } } function gasOrDefault(gas) { if (gas !== undefined) { return gas.toString(); } // Default of 250 TGAS return (TGAS * 250n).toString(); }