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