From 675f0af04c0372cbbb372c8922102dd86a926ae9 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 18 Oct 2024 12:27:39 +0200 Subject: [PATCH 1/4] Expose Signature Recovery from Near TxHash --- src/util.ts | 61 ++++++++++++++++++++++++++++++++++++++-- tests/unit/utils.spec.ts | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/util.ts b/src/util.ts index 0e08b7f..6074122 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,10 @@ -import { EthTransactionParams, Network, SessionRequestParams } from "near-ca"; +import { + EthTransactionParams, + getNetworkId, + Network as EvmNetwork, + SessionRequestParams, + signatureFromTxHash as sigFromHash, +} from "near-ca"; import { Address, Hex, @@ -11,6 +17,7 @@ import { zeroAddress, toBytes, keccak256, + serializeSignature, } from "viem"; import { PaymasterData, MetaTransaction } from "./types"; @@ -59,7 +66,7 @@ export async function isContract( } export function getClient(chainId: number): PublicClient { - return Network.fromChainId(chainId).client; + return EvmNetwork.fromChainId(chainId).client; } export function metaTransactionsFromRequest( @@ -94,3 +101,53 @@ export function saltNonceFromMessage(input: string): string { // Return string for readability and transport. return BigInt(keccak256(toBytes(input))).toString(); } + +export async function signatureFromTxHash( + nearTxHash: string, + accountId?: string +): Promise { + if (accountId) { + const signature = await sigFromHash( + `https://archival-rpc.${getNetworkId(accountId)}.near.org`, + nearTxHash, + accountId + ); + return packSignature(serializeSignature(signature)); + } + + try { + const signature = await raceToFirstResolve( + ["testnet", "mainnet"].map((networkId) => + sigFromHash(archiveNode(networkId), nearTxHash) + ) + ); + return packSignature(serializeSignature(signature)); + } catch { + throw new Error(`No signature found for txHash ${nearTxHash}`); + } +} + +const archiveNode = (networkId: string): string => + `https://archival-rpc.${networkId}.near.org`; + +export async function raceToFirstResolve( + promises: Promise[] +): Promise { + return new Promise((resolve, reject) => { + let rejectionCount = 0; + const totalPromises = promises.length; + + promises.forEach((promise) => { + // Wrap each promise so it only resolves when fulfilled + Promise.resolve(promise) + .then(resolve) // Resolve when any promise resolves + .catch(() => { + rejectionCount++; + // If all promises reject, reject the race with an error + if (rejectionCount === totalPromises) { + reject(new Error("All promises rejected")); + } + }); + }); + }); +} diff --git a/tests/unit/utils.spec.ts b/tests/unit/utils.spec.ts index 3659ee4..cabf233 100644 --- a/tests/unit/utils.spec.ts +++ b/tests/unit/utils.spec.ts @@ -10,6 +10,8 @@ import { packPaymasterData, packSignature, saltNonceFromMessage, + raceToFirstResolve, + signatureFromTxHash, } from "../../src/util"; describe("Utility Functions (mostly byte packing)", () => { @@ -81,4 +83,60 @@ describe("Utility Functions (mostly byte packing)", () => { "26371153660914144112327059280066269158753782528888197421682303285265580464377" ); }); + + it("raceToFirstResolve (success)", async () => { + // Example usage: + const promise1 = new Promise((_, reject) => + setTimeout(() => reject(new Error("Reject 1")), 100) + ); + const promise2 = new Promise((_, reject) => + setTimeout(() => reject(new Error("Reject 2")), 200) + ); + const promise3 = new Promise((resolve) => + setTimeout(resolve, 1, "Resolve") + ); + + await expect( + raceToFirstResolve([promise1, promise2, promise3]) + ).resolves.toBe("Resolve"); + }, 10); + it("raceToFirstResolve (failure)", async () => { + const promise1 = new Promise((_, reject) => + setTimeout(() => reject(new Error("Reject 1")), 10) + ); + const promise2 = new Promise((_, reject) => + setTimeout(() => reject(new Error("Reject 2")), 20) + ); + await expect(raceToFirstResolve([promise1, promise2])).rejects.toThrow( + "All promises rejected" + ); + }); + + it("signatureFromTxHash (mainnet)", async () => { + expect( + await signatureFromTxHash( + "BoKuHRFZ9qZ8gZRCcNS92mQYhEVbHrsUwed6D6CHELhv", + "ping-account.near" + ) + ).toBe( + "0x000000000000000000000000039ae6baaf4e707ca6d7cfe1fec3f1aa1b4978eb34224b347904b9e957a8dbd720da770464a68e3d1bcef1a4a46c3f9d0a358ccaa01669636f765364a17c03f61b" + ); + }); + + it("signatureFromTxHash (testnet)", async () => { + expect( + await signatureFromTxHash( + "BbmJk8W6FNz7cRcFxfVMpBWn9Q9uh99KLkzVyJwmPve8", + "neareth-dev.testnet" + ) + ).toBe( + "0x000000000000000000000000c69b46c006739fa11f3937556fbf7fc846359547c4927cfbc17eea108e13f5340c200541e90ab5d048087ea0e04f11f715115b362df692bf438b59623c574bc11b" + ); + }); + + it("signatureFromTxHash (rejects - doesn't exist)", async () => { + await expect(signatureFromTxHash("fart")).rejects.toThrow( + "No signature found for txHash fart" + ); + }); }); From d58b795e715cf67673246ce29fe5df08a430487d Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 18 Oct 2024 12:44:34 +0200 Subject: [PATCH 2/4] add doc strings --- src/util.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 6074122..3308f51 100644 --- a/src/util.ts +++ b/src/util.ts @@ -22,7 +22,6 @@ import { import { PaymasterData, MetaTransaction } from "./types"; -// export const PLACEHOLDER_SIG = encodePacked(["uint48", "uint48"], [0, 0]); type IntLike = Hex | bigint | string | number; @@ -102,6 +101,19 @@ export function saltNonceFromMessage(input: string): string { return BigInt(keccak256(toBytes(input))).toString(); } +/** + * Fetches the signature for a NEAR transaction hash. If an `accountId` is provided, + * it fetches the signature from the appropriate network. Otherwise, it races across + * both `testnet` and `mainnet`. + * + * @param {string} nearTxHash - The NEAR transaction hash for which to fetch the signature. + * @param {string} [accountId] - (Optional) The account ID associated with the transaction. + * Providing this will reduce dangling promises as the network is determined by the account. + * + * @returns {Promise} A promise that resolves to the hex-encoded signature. + * + * @throws Will throw an error if no signature is found for the given transaction hash. + */ export async function signatureFromTxHash( nearTxHash: string, accountId?: string @@ -127,9 +139,27 @@ export async function signatureFromTxHash( } } +/** + * Utility function to construct an archive node URL for a given NEAR network. + * + * @param {string} networkId - The ID of the NEAR network (e.g., 'testnet', 'mainnet'). + * + * @returns {string} The full URL of the archival RPC node for the specified network. + */ const archiveNode = (networkId: string): string => `https://archival-rpc.${networkId}.near.org`; +/** + * Races an array of promises and resolves with the first promise that fulfills. + * If all promises reject, the function will reject with an error. + * + * @template T + * @param {Promise[]} promises - An array of promises to race. Each promise should resolve to type `T`. + * + * @returns {Promise} A promise that resolves to the value of the first successfully resolved promise. + * + * @throws Will throw an error if all promises reject with the message "All promises rejected". + */ export async function raceToFirstResolve( promises: Promise[] ): Promise { From 2dcebbd6257b5f79c678788c2b42a280d86d2a4f Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 18 Oct 2024 12:47:35 +0200 Subject: [PATCH 3/4] revert unnecessary change --- src/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util.ts b/src/util.ts index 3308f51..8f0b55f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,7 @@ import { EthTransactionParams, getNetworkId, - Network as EvmNetwork, + Network, SessionRequestParams, signatureFromTxHash as sigFromHash, } from "near-ca"; @@ -65,7 +65,7 @@ export async function isContract( } export function getClient(chainId: number): PublicClient { - return EvmNetwork.fromChainId(chainId).client; + return Network.fromChainId(chainId).client; } export function metaTransactionsFromRequest( From 448db95958ea679ac0a87f9dc0d1dc9a2c39e9b2 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 18 Oct 2024 12:59:27 +0200 Subject: [PATCH 4/4] simplify var name --- src/util.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/util.ts b/src/util.ts index 8f0b55f..1b1ccec 100644 --- a/src/util.ts +++ b/src/util.ts @@ -106,7 +106,7 @@ export function saltNonceFromMessage(input: string): string { * it fetches the signature from the appropriate network. Otherwise, it races across * both `testnet` and `mainnet`. * - * @param {string} nearTxHash - The NEAR transaction hash for which to fetch the signature. + * @param {string} txHash - The NEAR transaction hash for which to fetch the signature. * @param {string} [accountId] - (Optional) The account ID associated with the transaction. * Providing this will reduce dangling promises as the network is determined by the account. * @@ -115,13 +115,13 @@ export function saltNonceFromMessage(input: string): string { * @throws Will throw an error if no signature is found for the given transaction hash. */ export async function signatureFromTxHash( - nearTxHash: string, + txHash: string, accountId?: string ): Promise { if (accountId) { const signature = await sigFromHash( `https://archival-rpc.${getNetworkId(accountId)}.near.org`, - nearTxHash, + txHash, accountId ); return packSignature(serializeSignature(signature)); @@ -129,13 +129,13 @@ export async function signatureFromTxHash( try { const signature = await raceToFirstResolve( - ["testnet", "mainnet"].map((networkId) => - sigFromHash(archiveNode(networkId), nearTxHash) + ["testnet", "mainnet"].map((network) => + sigFromHash(archiveNode(network), txHash) ) ); return packSignature(serializeSignature(signature)); } catch { - throw new Error(`No signature found for txHash ${nearTxHash}`); + throw new Error(`No signature found for txHash ${txHash}`); } }