UNPKG

near-ca

Version:

An SDK for controlling Ethereum Accounts from a Near Account.

137 lines (136 loc) 5.11 kB
/** * Retrieves a signature from a transaction hash * * @param nodeUrl - URL of the NEAR node * @param txHash - Transaction hash to query * @param accountId - Account ID used to determine shard for query (defaults to "non-empty") * @returns The signature from the transaction * @throws Error if HTTP request fails or response is invalid */ export async function signatureFromTxHash(nodeUrl, txHash, /// This field doesn't appear to be necessary although (possibly for efficiency), /// the docs mention that it is "used to determine which shard to query for transaction". accountId = "non-empty") { const payload = { jsonrpc: "2.0", id: "dontcare", // This could be replaced with `tx`. // method: "tx", method: "EXPERIMENTAL_tx_status", params: [txHash, accountId], }; // Make the POST request with the fetch API const response = await fetch(nodeUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const json = await response.json(); if (json.error) { throw new Error(`JSON-RPC error: ${json.error.message}`); } if (typeof json.result?.status === "object" && "Failure" in json.result.status) { const message = JSON.stringify(json.result.status.Failure); throw new Error(`Signature Request Failed in ${txHash} with message: ${message}`); } if (json.result) { return signatureFromOutcome(json.result); } else { throw new Error(`No FinalExecutionOutcome in response: ${json}`); } } /** * Transforms an MPC signature into a standard Ethereum signature * * @param mpcSig - The MPC signature to transform * @returns Standard Ethereum signature */ export function transformSignature(mpcSig) { const { big_r, s, recovery_id } = mpcSig; return { r: `0x${big_r.affine_point.substring(2)}`, s: `0x${s.scalar}`, yParity: recovery_id, }; } /** * Extracts a signature from a transaction outcome * * @param outcome - Transaction outcome from NEAR API * @returns The extracted signature * @throws Error if signature is not found or is invalid * @remarks * Handles both standard and relayed signature requests. For relayed requests, * extracts signature from receipts_outcome, taking the second occurrence as * the first is nested inside `{ Ok: MPCSignature }`. */ export function signatureFromOutcome( // The Partial object is intended to make up for the // difference between all the different near-api versions and wallet-selector bullshit // the field `final_execution_status` is in one, but not the other, and we don't use it anyway. outcome) { const txHash = outcome.transaction_outcome?.id; // TODO - find a scenario when outcome.status is `FinalExecutionStatusBasic`! let b64Sig = outcome.status.SuccessValue; if (!b64Sig) { // This scenario occurs when sign call is relayed (i.e. executed by someone else). // E.g. https://testnet.nearblocks.io/txns/G1f1HVUxDBWXAEimgNWobQ9yCx1EgA2tzYHJBFUfo3dj // We have to dig into `receipts_outcome` and extract the signature from within. // We want the second occurrence of the signature because // the first is nested inside `{ Ok: MPCSignature }`) b64Sig = outcome.receipts_outcome // Map to get SuccessValues: The Signature will appear twice. .map((receipt) => receipt.outcome.status.SuccessValue) // Reverse to "find" the last non-empty value! .reverse() .find((value) => value && value.trim().length > 0); } if (!b64Sig) { throw new Error(`No detectable signature found in transaction ${txHash}`); } if (b64Sig === "eyJFcnIiOiJGYWlsZWQifQ==") { // {"Err": "Failed"} throw new Error(`Signature Request Failed in ${txHash}`); } const decodedValue = Buffer.from(b64Sig, "base64").toString("utf-8"); const signature = JSON.parse(decodedValue); if (isMPCSignature(signature)) { return transformSignature(signature); } else { throw new Error(`No detectable signature found in transaction ${txHash}`); } } /** * Type guard to check if an object is a valid MPC signature * E.g. * { * big_r: { * affine_point: * "0337F110D095850FD1D6451B30AF40C15A82566C7FA28997D3EF83C5588FBAF99C", * }, * s: { * scalar: * "4C5D1C3A8CAFF5F0C13E34B4258D114BBEAB99D51AF31648482B7597F3AD5B72", * }, * recovery_id: 1, * } * @param obj - The object to check * @returns True if the object matches MPCSignature structure */ function isMPCSignature(obj) { return (typeof obj === "object" && obj !== null && typeof obj.big_r === "object" && typeof obj.big_r.affine_point === "string" && typeof obj.s === "object" && typeof obj.s.scalar === "string" && typeof obj.recovery_id === "number"); }