near-safe
Version:
An SDK for controlling Ethereum Smart Accounts via ERC4337 from a Near Account.
164 lines (163 loc) • 6.78 kB
JavaScript
import { concat, createPublicClient, encodeFunctionData, encodePacked, getAddress, getCreate2Address, http, keccak256, parseAbi, toHex, zeroAddress, } from "viem";
import { SAFE_DEPLOYMENTS } from "../_gen/deployments";
import { DEFAULT_SETUP_RPC, SENTINEL_OWNERS, USER_OP_IDENTIFIER, } from "../constants";
import { PLACEHOLDER_SIG, getClient, packGas, packPaymasterData, } from "../util";
/**
* All contracts used in account creation & execution
*/
export class SafeContractSuite {
// Used only for stateless contract reads.
setupClient;
singleton;
proxyFactory;
m4337;
moduleSetup;
entryPoint;
constructor(rpcUrl = DEFAULT_SETUP_RPC) {
this.setupClient = createPublicClient({ transport: http(rpcUrl) });
const deployments = SAFE_DEPLOYMENTS;
this.singleton = deployments.singleton;
this.proxyFactory = deployments.proxyFactory;
this.m4337 = deployments.m4337;
this.moduleSetup = deployments.moduleSetup;
this.entryPoint = deployments.entryPoint;
}
async addressForSetup(setup, saltNonce) {
// bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
// cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58
const salt = keccak256(encodePacked(["bytes32", "uint256"], [keccak256(setup), BigInt(saltNonce)]));
// abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));
// cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29
const initCode = encodePacked(["bytes", "uint256"], [
(await this.setupClient.readContract({
address: this.proxyFactory.address,
abi: this.proxyFactory.abi,
functionName: "proxyCreationCode",
})),
BigInt(this.singleton.address),
]);
return getCreate2Address({
from: this.proxyFactory.address,
salt,
bytecodeHash: keccak256(initCode),
});
}
getSetup(owners) {
return encodeFunctionData({
abi: this.singleton.abi,
functionName: "setup",
args: [
owners, // _owners
1, // _threshold
this.moduleSetup.address, // to
encodeFunctionData({
abi: this.moduleSetup.abi,
functionName: "enableModules",
args: [[this.m4337.address]],
}), // data
this.m4337.address, // fallbackHandler
zeroAddress, // paymentToken
0, // payment
zeroAddress, // paymentReceiver
],
});
}
addOwnerData(newOwner) {
return encodeFunctionData({
abi: this.singleton.abi,
functionName: "addOwnerWithThreshold",
args: [newOwner, 1],
});
}
async removeOwnerData(chainId, safeAddress, owner) {
const prevOwner = await this.prevOwner(chainId, safeAddress, owner);
return encodeFunctionData({
abi: this.singleton.abi,
functionName: "removeOwner",
// Keep threshold at 1!
args: [prevOwner, owner, 1],
});
}
async getOpHash(chainId, unsignedUserOp) {
const { factory, factoryData, verificationGasLimit, callGasLimit, maxPriorityFeePerGas, maxFeePerGas, } = unsignedUserOp;
const client = await getClient(chainId);
const opHash = await client.readContract({
address: this.m4337.address,
abi: this.m4337.abi,
functionName: "getOperationHash",
args: [
{
...unsignedUserOp,
initCode: factory
? encodePacked(["address", "bytes"], [factory, factoryData])
: "0x",
accountGasLimits: packGas(verificationGasLimit, callGasLimit),
gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas),
paymasterAndData: packPaymasterData(unsignedUserOp),
signature: PLACEHOLDER_SIG,
},
],
});
return opHash;
}
factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce) {
return safeNotDeployed
? {
factory: this.proxyFactory.address,
factoryData: encodeFunctionData({
abi: this.proxyFactory.abi,
functionName: "createProxyWithNonce",
args: [this.singleton.address, setup, safeSaltNonce],
}),
}
: {};
}
async buildUserOp(nonce, txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
return {
sender: safeAddress,
nonce: toHex(nonce),
...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce),
// <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
callData: concat([
encodeFunctionData({
abi: this.m4337.abi,
functionName: "executeUserOp",
args: [
getAddress(txData.to),
BigInt(txData.value),
txData.data,
txData.operation || 0,
],
}),
// Append On-Chain Identifier:
USER_OP_IDENTIFIER,
]),
...feeData,
};
}
async getNonce(address, chainId) {
const nonce = (await getClient(chainId).readContract({
abi: this.entryPoint.abi,
address: this.entryPoint.address,
functionName: "getNonce",
args: [address, 0],
}));
return nonce;
}
async prevOwner(chainId, safeAddress, owner) {
const client = getClient(chainId);
const currentOwners = await client.readContract({
address: safeAddress,
// abi: this.singleton.abi,
abi: parseAbi([
"function getOwners() public view returns (address[] memory)",
]),
functionName: "getOwners",
});
const ownerIndex = currentOwners.findIndex((t) => t === owner);
if (ownerIndex === -1) {
throw new Error(`Not a current owner: ${owner}`);
}
return ownerIndex > 0 ? currentOwners[ownerIndex - 1] : SENTINEL_OWNERS;
}
}