From f9aaa172f33cf8e9a08ccfbecf7dba20525775e3 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 7 May 2024 14:50:02 +0200 Subject: [PATCH] Extract Signature By Near-TxHash (#29) --- .github/workflows/ci.yaml | 18 ++--- src/chains/ethereum.ts | 2 +- src/index.ts | 2 +- src/mpcContract.ts | 2 +- src/types/rpc.ts | 133 +++++++++++++++++++++++++++++++ src/{ => types}/types.ts | 2 +- src/utils/gasPrice.ts | 2 +- src/utils/getSignature.ts | 37 +++++++++ src/utils/transaction.ts | 2 +- tests/utils.getSignature.test.ts | 21 +++++ 10 files changed, 205 insertions(+), 16 deletions(-) create mode 100644 src/types/rpc.ts rename src/{ => types}/types.ts (97%) create mode 100644 src/utils/getSignature.ts create mode 100644 tests/utils.getSignature.test.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b683e3f..843822b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,15 +20,13 @@ jobs: - name: Install & Build run: yarn && yarn build - - name: Lint & Test + - name: Lint & Unit Test run: | yarn lint - yarn test - env: - NODE_URL: https://rpc2.sepolia.org - SCAN_URL: https://sepolia.etherscan.io - GAS_STATION_URL: https://sepolia.beaconcha.in/api/v1/execution/gasnow - - NEAR_MULTICHAIN_CONTRACT: multichain-testnet-2.testnet - NEAR_ACCOUNT_ID: ${{secrets.NEAR_ACCOUNT_ID}} - NEAR_ACCOUNT_PRIVATE_KEY: ${{secrets.NEAR_PK}} + yarn test utils + # These are only relevant for e2e tests + # env: + # NEAR_MULTICHAIN_CONTRACT: multichain-testnet-2.testnet + # NEAR_MULTICHAIN_CONTRACT: v5.multichain-mpc-dev.testnet + # NEAR_ACCOUNT_ID: ${{secrets.NEAR_ACCOUNT_ID}} + # NEAR_ACCOUNT_PRIVATE_KEY: ${{secrets.NEAR_PK}} diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts index 3eb0f6f..7aacbcc 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/ethereum.ts @@ -21,7 +21,7 @@ import { NearContractFunctionPayload, TxPayload, TransactionWithSignature, -} from "../types"; +} from "../types/types"; import { MultichainContract } from "../mpcContract"; import BN from "bn.js"; import { queryGasPrice } from "../utils/gasPrice"; diff --git a/src/index.ts b/src/index.ts index 74b4ab4..591824b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from "./chains/ethereum"; export * from "./mpcContract"; export * from "./chains/near"; -export * from "./types"; +export * from "./types/types"; export * from "./network"; diff --git a/src/mpcContract.ts b/src/mpcContract.ts index 4415ac3..1b0e48e 100644 --- a/src/mpcContract.ts +++ b/src/mpcContract.ts @@ -12,7 +12,7 @@ import { MPCSignature, NearContractFunctionPayload, SignArgs, -} from "./types"; +} from "./types/types"; interface MultichainContractInterface extends Contract { // Define the signature for the `public_key` view method diff --git a/src/types/rpc.ts b/src/types/rpc.ts new file mode 100644 index 0000000..984aa65 --- /dev/null +++ b/src/types/rpc.ts @@ -0,0 +1,133 @@ +// Basic structure of the JSON-RPC response +export interface JSONRPCResponse { + jsonrpc: string; + result: Result; + id: string; +} + +// Result contains various fields like final execution status, an array of receipts, etc. +export interface Result { + final_execution_status: string; + receipts: Receipt[]; + receipts_outcome: ReceiptOutcome[]; + status: TransactionStatus; + transaction: Transaction; + transaction_outcome: TransactionOutcome; +} + +// Define Receipt type with its structure +interface Receipt { + predecessor_id: string; + receipt: ReceiptDetail; + receipt_id: string; + receiver_id: string; +} + +// Detailed structure of a receipt which includes actions and other properties +interface ReceiptDetail { + Action: ActionDetail; +} + +// Actions within the receipt +interface ActionDetail { + actions: Action[]; + gas_price: string; + // TODO - determine types here and find a non-trivial example. + // cf: https://github.com/Mintbase/near-ca/issues/31 + // input_data_ids: any[]; + // output_data_receivers: any[]; + signer_id: string; + signer_public_key: string; +} + +// Action can have different types like FunctionCall or Transfer +interface Action { + FunctionCall?: FunctionCall; + Transfer?: Transfer; +} + +// FunctionCall action specifics +interface FunctionCall { + args: string; + deposit: string; + gas: number; + method_name: string; +} + +// Transfer action specifics +interface Transfer { + deposit: string; +} + +// Receipt outcomes are listed in an array +export interface ReceiptOutcome { + block_hash: string; + id: string; + outcome: Outcome; + proof: Proof[]; +} + +// Outcome of executing the action +interface Outcome { + executor_id: string; + gas_burnt: number; + logs: string[]; + metadata: Metadata; + receipt_ids: string[]; + status: OutcomeStatus; + tokens_burnt: string; +} + +// Metadata may contain various gas profiling information +interface Metadata { + gas_profile: GasProfile[]; + version: number; +} + +// Detailed gas usage per action or computation step +interface GasProfile { + cost: string; + cost_category: string; + gas_used: number; +} + +// Status of the outcome, success or failure specifics +interface OutcomeStatus { + SuccessReceiptId?: string; + SuccessValue?: string; +} + +// Proofs for the transaction validation +interface Proof { + direction: string; + hash: string; +} + +// Status field detailing the transaction execution result +interface TransactionStatus { + SuccessValue: string; +} + +// Transaction detail structure +interface Transaction { + actions: TransactionAction[]; + hash: string; + nonce: number; + public_key: string; + receiver_id: string; + signature: string; + signer_id: string; +} + +// Actions within a transaction +interface TransactionAction { + FunctionCall: FunctionCall; +} + +// Transaction outcome mirrors structure similar to receipt outcomes +interface TransactionOutcome { + block_hash: string; + id: string; + outcome: Outcome; + proof: Proof[]; +} diff --git a/src/types.ts b/src/types/types.ts similarity index 97% rename from src/types.ts rename to src/types/types.ts index a1fe3e9..60d1be1 100644 --- a/src/types.ts +++ b/src/types/types.ts @@ -1,4 +1,4 @@ -import { MultichainContract } from "./mpcContract"; +import { MultichainContract } from "../mpcContract"; import { FunctionCallAction } from "@near-wallet-selector/core"; import BN from "bn.js"; import { Hex } from "viem"; diff --git a/src/utils/gasPrice.ts b/src/utils/gasPrice.ts index 8526e42..a0bb62f 100644 --- a/src/utils/gasPrice.ts +++ b/src/utils/gasPrice.ts @@ -1,4 +1,4 @@ -import { GasPrices } from "../types"; +import { GasPrices } from "../types/types"; interface GasPriceResponse { code: number; diff --git a/src/utils/getSignature.ts b/src/utils/getSignature.ts new file mode 100644 index 0000000..f6f7cff --- /dev/null +++ b/src/utils/getSignature.ts @@ -0,0 +1,37 @@ +import { JSONRPCResponse } from "../types/rpc"; + +export async function signatureFromTxHash( + nodeUrl: string, + txHash: string, + /// 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: string = "non-empty" +): Promise<[string, string]> { + const payload = { + jsonrpc: "2.0", + id: "dontcare", + // This could be replaced with `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), + }); + + const jsonResponse = (await response.json()) as JSONRPCResponse; + const base64Sig = jsonResponse.result.status.SuccessValue; + + if (base64Sig) { + // Decode from base64 + const decodedValue = Buffer.from(base64Sig, "base64").toString("utf-8"); + return JSON.parse(decodedValue); + } else { + throw new Error("No valid values found in the array."); + } +} diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index 5fc5502..dea6d89 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -8,7 +8,7 @@ import { serializeTransaction, signatureToHex, } from "viem"; -import { TransactionWithSignature } from "../types"; +import { TransactionWithSignature } from "../types/types"; import { secp256k1 } from "@noble/curves/secp256k1"; import { publicKeyToAddress } from "viem/utils"; diff --git a/tests/utils.getSignature.test.ts b/tests/utils.getSignature.test.ts new file mode 100644 index 0000000..47d2027 --- /dev/null +++ b/tests/utils.getSignature.test.ts @@ -0,0 +1,21 @@ +import { signatureFromTxHash } from "../src/utils/getSignature"; + +describe("utility: get Signature", () => { + const url: string = "https://archival-rpc.testnet.near.org"; + // const accountId = "neareth-dev.testnet"; + const successHash = "88LS5pkj99pd6B6noZU6sagQ1QDwHHoSy3qpHr5xLNsR"; + const failedHash = "HaG9L4HnP69v6wSnAmKfzsCUhDaVMRZWNGhGqnepsMTD"; + + it("successful: signatureFromTxHash", async () => { + const sig = await signatureFromTxHash(url, successHash); + expect(sig).toEqual([ + "03EA06CECA2B7D71F6F4DA729A681B4DE44C6402F5F5BB9FC88C6706959D4FEDD4", + "67986E234DEC5D51CF6AED452FE1C4544924218AC20B009F81BAAE53C02AFE76", + ]); + }); + it("failed: signatureFromTxHash", async () => { + await expect(signatureFromTxHash(url, failedHash)).rejects.toThrow( + "No valid values found in the array." + ); + }); +});