UNPKG

near-safe

Version:

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

376 lines (375 loc) 18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NearSafe = void 0; const near_ca_1 = require("near-ca"); const viem_1 = require("viem"); const constants_1 = require("./constants"); const bundler_1 = require("./lib/bundler"); const multisend_1 = require("./lib/multisend"); const safe_1 = require("./lib/safe"); const safe_message_1 = require("./lib/safe-message"); const util_1 = require("./util"); class NearSafe { /** * Creates a new instance of the `NearSafe` class using the provided configuration. * * @param {NearSafeConfig} config - The configuration object required to initialize the `NearSafe` instance, including the Pimlico key and safe salt nonce. * @returns {Promise<NearSafe>} - A promise that resolves to a new `NearSafe` instance. */ static async create(config) { const { pimlicoKey, setupRpc } = config; const safeSaltNonce = config.safeSaltNonce || constants_1.DEFAULT_SAFE_SALT_NONCE; // const nearAdapter = await mockAdapter(); const nearAdapter = await (0, near_ca_1.setupAdapter)(config.mpc); const safePack = new safe_1.SafeContractSuite(setupRpc); const setup = safePack.getSetup([nearAdapter.address]); const safeAddress = await safePack.addressForSetup(setup, safeSaltNonce); return new NearSafe(nearAdapter, safePack, pimlicoKey, setup, safeAddress, safeSaltNonce); } /** * Constructs a new `NearSafe` object with the provided parameters. * * @param {NearEthAdapter} nearAdapter - The NEAR adapter used for interacting with the NEAR blockchain. * @param {SafeContractSuite} safePack - A suite of contracts and utilities for managing the Safe contract. * @param {string} pimlicoKey - A key required for authenticating with the Pimlico service. * @param {string} setup - The setup string generated for the Safe contract. * @param {Address} safeAddress - The address of the deployed Safe contract. * @param {string} safeSaltNonce - A unique nonce used to differentiate the Safe setup. */ constructor(nearAdapter, safePack, pimlicoKey, setup, safeAddress, safeSaltNonce) { this.nearAdapter = nearAdapter; this.address = safeAddress; this.setup = setup; this.safePack = safePack; this.pimlicoKey = pimlicoKey; this.safeSaltNonce = safeSaltNonce; } /** * Retrieves the MPC (Multi-Party Computation) address associated with the NEAR adapter. * * @returns {Address} - The MPC address of the NEAR adapter. */ get mpcAddress() { return this.nearAdapter.address; } /** * Retrieves the contract ID of the MPC contract associated with the NEAR adapter. * * @returns {string} - The contract ID of the MPC contract. */ get mpcContractId() { return this.nearAdapter.mpcContract.accountId(); } /** * Retrieves the balance of the Safe account on the specified EVM chain. * * @param {number} chainId - The ID of the blockchain network where the Safe account is located. * @returns {Promise<bigint>} - A promise that resolves to the balance of the Safe account in wei. */ async getBalance(chainId) { return await (0, util_1.getClient)(chainId).getBalance({ address: this.address }); } /** * Constructs a user operation for the specified chain, including necessary gas fees, nonce, and paymaster data. * Warning: Uses a private ethRPC with sensitive Pimlico API key (should be run server side). * * @param {Object} args - The arguments for building the transaction. * @param {number} args.chainId - The ID of the blockchain network where the transaction will be executed. * @param {MetaTransaction[]} args.transactions - A list of meta-transactions to be included in the user operation. * @param {boolean} args.usePaymaster - Flag indicating whether to use a paymaster for gas fees. If true, the transaction will be sponsored by the paymaster. * @returns {Promise<UserOperation>} - A promise that resolves to a complete `UserOperation` object, including gas fees, nonce, and paymaster data. * @throws {Error} - Throws an error if the transaction set is empty or if any operation fails during the building process. */ async buildTransaction(args) { const { transactions, sponsorshipPolicy, chainId } = args; if (transactions.length === 0) { throw new Error("Empty transaction set!"); } const bundler = this.bundlerForChainId(chainId); const [gasFees, nonce, safeDeployed] = await Promise.all([ bundler.getGasPrice(), this.safePack.getNonce(this.address, chainId), this.safeDeployed(chainId), ]); // Build Singular MetaTransaction for Multisend from transaction list. const tx = transactions.length > 1 ? (0, multisend_1.encodeMulti)(transactions) : transactions[0]; const rawUserOp = await this.safePack.buildUserOp(nonce, tx, this.address, gasFees.fast, this.setup, !safeDeployed, this.safeSaltNonce); const paymasterData = await bundler.getPaymasterData(rawUserOp, sponsorshipPolicy); const unsignedUserOp = { ...rawUserOp, ...paymasterData }; return unsignedUserOp; } /** * Signs a transaction with the NEAR adapter using the provided operation hash. * * @param {Hex} safeOpHash - The hash of the user operation that needs to be signed. * @returns {Promise<Hex>} - A promise that resolves to the packed signature in hexadecimal format. */ async signTransaction(safeOpHash) { const signature = await this.nearAdapter.sign(safeOpHash); return (0, util_1.packSignature)(signature); } /** * Computes the operation hash for a given user operation. * * @param {UserOperation} userOp - The user operation for which the hash needs to be computed. * @returns {Promise<Hash>} - A promise that resolves to the hash of the provided user operation. */ async opHash(chainId, userOp) { return this.safePack.getOpHash(chainId, userOp); } /** * Encodes a request to sign a transaction using either a paymaster or the user's own funds. * * @param {SignRequestData} signRequest - The data required to create the signature request. This includes information such as the chain ID and other necessary fields for the transaction. * @param {boolean} usePaymaster - Flag indicating whether to use a paymaster for gas fees. If true, the transaction will be sponsored by the paymaster. * @returns {Promise<EncodedTxData>} - A promise that resolves to the encoded transaction data for the NEAR and EVM networks. */ async encodeSignRequest(signRequest, sponsorshipPolicy) { const { evmMessage, hashToSign } = await this.requestRouter(signRequest, sponsorshipPolicy); return { nearPayload: await this.nearAdapter.mpcContract.encodeSignatureRequestTx({ path: this.nearAdapter.derivationPath, payload: (0, near_ca_1.toPayload)(hashToSign), key_version: 0, }), evmData: { chainId: signRequest.chainId, evmMessage, hashToSign, }, }; } /** * Broadcasts a user operation to the EVM network with a provided signature. * Warning: Uses a private ethRPC with sensitive Pimlico API key (should be run server side). * * @param {number} chainId - The ID of the EVM network to which the transaction should be broadcasted. * @param {Signature} signature - The valid signature of the unsignedUserOp. * @param {UserOperation} unsignedUserOp - The unsigned user operation to be broadcasted. This includes transaction data such as the destination address and data payload. * @returns {Promise<Hash>} - A promise that resolves hash of the executed user operation. * @throws {Error} - Throws an error if the EVM broadcast fails, including the error message for debugging. */ async broadcastBundler(chainId, signatureHex, unsignedUserOp) { try { return this.executeTransaction(chainId, { ...unsignedUserOp, signature: (0, util_1.packSignature)(signatureHex), }); } catch (error) { throw new Error(`Failed Bundler broadcast: ${error instanceof Error ? error.message : String(error)}`); } } /** * Executes a user operation on the specified blockchain network. * Warning: Uses a private ethRPC with sensitive Pimlico API key (should be run server side). * * @param {number} chainId - The ID of the blockchain network on which to execute the transaction. * @param {UserOperation} userOp - The user operation to be executed, typically includes the data and signatures necessary for the transaction. * @returns {Promise<Hash>} - A promise that resolves to the hash of the executed transaction. */ async executeTransaction(chainId, userOp) { return this.bundlerForChainId(chainId).sendUserOperation(userOp); } /** * Retrieves the receipt of a previously executed user operation. * Warning: Uses a private ethRPC with sensitive Pimlico API key (should be run server side). * * @param {number} chainId - The ID of the blockchain network where the operation was executed. * @param {Hash} userOpHash - The hash of the user operation for which to retrieve the receipt. * @returns {Promise<UserOperationReceipt>} - A promise that resolves to the receipt of the user operation, which includes status and logs. */ async getOpReceipt(chainId, userOpHash) { return this.bundlerForChainId(chainId).getUserOpReceipt(userOpHash); } /** * Checks if the Safe contract is deployed on the specified chain. * * @param {number} chainId - The ID of the blockchain network where the Safe contract should be checked. * @returns {Promise<boolean>} - A promise that resolves to `true` if the Safe contract is deployed, otherwise `false`. */ async safeDeployed(chainId) { return (0, util_1.isContract)(this.address, chainId); } /** * Determines if the Safe account has sufficient funds to cover the transaction costs. * * @param {number} chainId - The ID of the blockchain network where the Safe account is located. * @param {MetaTransaction[]} transactions - A list of meta-transactions to be evaluated for funding. * @param {bigint} gasCost - The estimated gas cost of executing the transactions. * @returns {Promise<boolean>} - A promise that resolves to `true` if the Safe account has sufficient funds, otherwise `false`. */ async sufficientlyFunded(chainId, transactions, gasCost) { const txValue = transactions.reduce((acc, tx) => acc + BigInt(tx.value), 0n); if (txValue + gasCost === 0n) { return true; } const safeBalance = await this.getBalance(chainId); return txValue + gasCost < safeBalance; } /** * Creates a meta-transaction for adding a new owner to the Safe contract. * * @param {Address} address - The address of the new owner to be added. * @returns {MetaTransaction} - A meta-transaction object for adding the new owner. */ addOwnerTx(address) { return { to: this.address, value: "0", data: this.safePack.addOwnerData(address), }; } /** * Creates a meta-transaction for adding a new owner to the Safe contract. * * @param {number} chainId - the chainId to build the transaction for. * @param {Address} address - The address of the new owner to be removed. * @returns {Promise<MetaTransaction>} - A meta-transaction object for adding the new owner. */ async removeOwnerTx(chainId, address) { return { to: this.address, value: "0x00", data: await this.safePack.removeOwnerData(chainId, this.address, address), }; } /** * Creates and returns a new `Erc4337Bundler` instance for the specified chain. * * @param {number} chainId - The ID of the blockchain network for which the bundler is to be created. * @returns {Erc4337Bundler} - A new instance of the `Erc4337Bundler` class configured for the specified chain. */ bundlerForChainId(chainId) { return new bundler_1.Erc4337Bundler(this.safePack.entryPoint.address, this.pimlicoKey, chainId); } /** * Handles routing of signature requests based on the provided method, chain ID, and parameters. * * @async * @function requestRouter * @param {SignRequestData} params - An object containing the method, chain ID, and request parameters. * @returns {Promise<{ evmMessage: string; payload: number[]; recoveryData: RecoveryData }>} * - Returns a promise that resolves to an object containing the Ethereum Virtual Machine (EVM) message, * the payload (hashed data), and recovery data needed for reconstructing the signature request. */ async requestRouter({ method, chainId, params }, sponsorshipPolicy) { // Extract `from` based on the method and check uniqueness const fromAddresses = (() => { switch (method) { case "eth_signTypedData": case "eth_signTypedData_v4": case "eth_sign": return [params[0]]; case "personal_sign": return [params[1]]; case "eth_sendTransaction": return params.map((p) => p.from); default: return []; } })(); // Assert uniqueness (0, util_1.assertUnique)(fromAddresses); if (!fromAddresses[0]) { throw new Error("No from address provided"); } // Early return with eoaEncoding if `from` is not the Safe if (!this.encodeForSafe(fromAddresses[0])) { // TODO: near-ca needs to update this for typed data like we did. return (0, near_ca_1.requestRouter)({ method, chainId, params }); } const safeInfo = { address: { value: this.address }, chainId: chainId.toString(), // TODO: Should be able to read this from on chain. version: "1.4.1+L2", }; // TODO: We are provided with sender in the input, but also expect safeInfo. // We should either confirm they agree or ignore one of the two. switch (method) { case "eth_signTypedData": case "eth_signTypedData_v4": { const [_, typedDataString] = params; const message = (0, safe_message_1.decodeSafeMessage)(JSON.parse(typedDataString), safeInfo); return { evmMessage: JSON.parse(typedDataString), hashToSign: message.safeMessageHash, }; } case "eth_sign": { const [_, messageOrData] = params; const message = (0, safe_message_1.decodeSafeMessage)(messageOrData, safeInfo); return { evmMessage: message.decodedMessage, hashToSign: message.safeMessageHash, }; } case "personal_sign": { const [messageHash, _] = params; const message = (0, safe_message_1.decodeSafeMessage)(messageHash, safeInfo); return { evmMessage: message.decodedMessage, hashToSign: message.safeMessageHash, }; } case "eth_sendTransaction": { const castParams = params; const transactions = (0, util_1.metaTransactionsFromRequest)(castParams); const userOp = await this.buildTransaction({ chainId, transactions, ...(sponsorshipPolicy ? { sponsorshipPolicy } : {}), }); const opHash = await this.opHash(chainId, userOp); return { evmMessage: JSON.stringify(userOp), hashToSign: opHash, }; } } } encodeForSafe(from) { const lowerFrom = from.toLowerCase(); const lowerZero = viem_1.zeroAddress.toLowerCase(); const lowerSafe = this.address.toLowerCase(); const lowerMpc = this.mpcAddress.toLowerCase(); // We allow zeroAddress (and and treat is as from = safe) if (![lowerSafe, lowerMpc, lowerZero].includes(lowerFrom)) { throw new Error(`Unexpected from address ${from} - expected {}`); } return [this.address.toLowerCase(), lowerZero].includes(lowerFrom); } async policiesForChainId(chainId) { return this.bundlerForChainId(chainId).getSponsorshipPolicies(); } deploymentRequest(chainId) { return { method: "eth_sendTransaction", chainId, params: [ { from: this.address, to: viem_1.zeroAddress, value: "0x0", data: "0x", }, ], }; } addOwnerRequest(chainId, recoveryAddress) { return { method: "eth_sendTransaction", chainId, params: [ { from: this.address, to: this.address, value: "0x0", data: new safe_1.SafeContractSuite().addOwnerData(recoveryAddress), }, ], }; } } exports.NearSafe = NearSafe;