near-safe
Version:
An SDK for controlling Ethereum Smart Accounts via ERC4337 from a Near Account.
150 lines (149 loc) • 6.4 kB
JavaScript
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;
}