UNPKG

near-safe

Version:

An SDK for controlling Ethereum Smart Accounts via ERC4337 from a Near Account.

150 lines (149 loc) 6.4 kB
import { getNetworkId, Network, signatureFromTxHash as sigFromHash, } from "near-ca"; import { concatHex, encodePacked, toHex, isHex, parseTransaction, zeroAddress, toBytes, keccak256, serializeSignature, } from "viem"; export const PLACEHOLDER_SIG = encodePacked(["uint48", "uint48"], [0, 0]); export const packGas = (hi, lo) => encodePacked(["uint128", "uint128"], [BigInt(hi), BigInt(lo)]); export function packSignature(signature, validFrom = 0, validTo = 0) { return encodePacked(["uint48", "uint48", "bytes"], [validFrom, validTo, signature]); } export function packPaymasterData(data) { return (data.paymaster ? concatHex([ data.paymaster, toHex(BigInt(data.paymasterVerificationGasLimit || 0n), { size: 16 }), toHex(BigInt(data.paymasterPostOpGasLimit || 0n), { size: 16 }), data.paymasterData || "0x", ]) : "0x"); } export function containsValue(transactions) { return transactions.some((tx) => BigInt(tx.value) !== 0n); } export async function isContract(address, chainId) { return (await getClient(chainId).getCode({ address })) !== undefined; } export function getClient(chainId, rpcUrl) { // Caution: rpcUrl might not be aligned with chainId! const options = rpcUrl ? { rpcUrl } : {}; return Network.fromChainId(chainId, options).client; } export function metaTransactionsFromRequest(params) { let transactions; if (isHex(params)) { // TODO: Consider deprecating this route. // If RLP hex is given, decode the transaction and build EthTransactionParams const tx = parseTransaction(params); transactions = [ { from: zeroAddress, // TODO: This is a hack - but its unused. to: tx.to, value: tx.value ? toHex(tx.value) : "0x00", data: tx.data || "0x", }, ]; } else { // TODO: add type guard here. transactions = params; } return transactions.map((tx) => ({ to: tx.to, value: tx.value || "0x00", data: tx.data || "0x", })); } export function saltNonceFromMessage(input) { // Convert the string to bytes (UTF-8 encoding) // Compute the keccak256 hash of the input bytes // Convert the resulting hash (which is in hex) to a BigInt // Return string for readability and transport. 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} 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. * * @returns {Promise<Hex>} 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(txHash, accountId) { if (accountId) { const signature = await sigFromHash(`https://archival-rpc.${getNetworkId(accountId)}.near.org`, txHash, accountId); return packSignature(serializeSignature(signature)); } try { const signature = await raceToFirstResolve(["testnet", "mainnet"].map((network) => sigFromHash(archiveNode(network), txHash))); return packSignature(serializeSignature(signature)); } catch { throw new Error(`No signature found for txHash ${txHash}`); } } /** * 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) => `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<T>[]} promises - An array of promises to race. Each promise should resolve to type `T`. * * @returns {Promise<T>} 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) { 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")); } }); }); }); } export function assertUnique(iterable, errorMessage = "The collection contains more than one distinct element.") { const uniqueValues = new Set(iterable); if (uniqueValues.size > 1) { throw new Error(errorMessage); } } export function userOpTransactionCost(userOp) { // Convert values from hex to decimal const preVerificationGas = BigInt(userOp.preVerificationGas); const verificationGasLimit = BigInt(userOp.verificationGasLimit); const callGasLimit = BigInt(userOp.callGasLimit); const paymasterVerificationGasLimit = BigInt(userOp.paymasterVerificationGasLimit || "0x0"); const paymasterPostOpGasLimit = BigInt(userOp.paymasterPostOpGasLimit || "0x0"); // Sum total gas const totalGasUsed = preVerificationGas + verificationGasLimit + callGasLimit + paymasterVerificationGasLimit + paymasterPostOpGasLimit; // Convert maxFeePerGas from hex to decimal const maxFeePerGas = BigInt(userOp.maxFeePerGas); // Calculate total cost in wei const totalCostInWei = totalGasUsed * maxFeePerGas; // Convert to Ether for a human-readable value return totalCostInWei; }