near-safe 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,37 @@
1
- import { ethers } from "ethers";
1
+ import { Address, Hash, PublicClient, Transport } from "viem";
2
2
  import { GasPrices, PaymasterData, UnsignedUserOperation, UserOperation, UserOperationReceipt } from "../types";
3
+ type BundlerRpcSchema = [
4
+ {
5
+ Method: "pm_sponsorUserOperation";
6
+ Parameters: [UnsignedUserOperation, Address];
7
+ ReturnType: PaymasterData;
8
+ },
9
+ {
10
+ Method: "eth_sendUserOperation";
11
+ Parameters: [UserOperation, Address];
12
+ ReturnType: Hash;
13
+ },
14
+ {
15
+ Method: "pimlico_getUserOperationGasPrice";
16
+ Parameters: [];
17
+ ReturnType: GasPrices;
18
+ },
19
+ {
20
+ Method: "eth_getUserOperationReceipt";
21
+ Parameters: [Hash];
22
+ ReturnType: UserOperationReceipt | null;
23
+ }
24
+ ];
3
25
  export declare class Erc4337Bundler {
4
- provider: ethers.JsonRpcProvider;
5
- entryPointAddress: string;
26
+ client: PublicClient<Transport, undefined, undefined, BundlerRpcSchema>;
27
+ entryPointAddress: Address;
6
28
  apiKey: string;
7
29
  chainId: number;
8
- constructor(entryPointAddress: string, apiKey: string, chainId: number);
9
- client(chainId: number): ethers.JsonRpcProvider;
30
+ constructor(entryPointAddress: Address, apiKey: string, chainId: number);
10
31
  getPaymasterData(rawUserOp: UnsignedUserOperation, usePaymaster: boolean, safeNotDeployed: boolean): Promise<PaymasterData>;
11
- sendUserOperation(userOp: UserOperation): Promise<string>;
32
+ sendUserOperation(userOp: UserOperation): Promise<Hash>;
12
33
  getGasPrice(): Promise<GasPrices>;
13
- _getUserOpReceiptInner(userOpHash: string): Promise<UserOperationReceipt | null>;
14
- getUserOpReceipt(userOpHash: string): Promise<UserOperationReceipt>;
34
+ getUserOpReceipt(userOpHash: Hash): Promise<UserOperationReceipt>;
35
+ private _getUserOpReceiptInner;
15
36
  }
37
+ export {};
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Erc4337Bundler = void 0;
4
- // TODO: Ethers dependency is only for Generic HTTP Provider
5
- const ethers_1 = require("ethers");
6
4
  const viem_1 = require("viem");
7
5
  const util_1 = require("../util");
8
6
  function bundlerUrl(chainId, apikey) {
@@ -13,41 +11,37 @@ class Erc4337Bundler {
13
11
  this.entryPointAddress = entryPointAddress;
14
12
  this.apiKey = apiKey;
15
13
  this.chainId = chainId;
16
- this.provider = new ethers_1.ethers.JsonRpcProvider(bundlerUrl(chainId, apiKey));
17
- }
18
- client(chainId) {
19
- return new ethers_1.ethers.JsonRpcProvider(bundlerUrl(chainId, this.apiKey));
14
+ this.client = (0, viem_1.createPublicClient)({
15
+ transport: (0, viem_1.http)(bundlerUrl(chainId, this.apiKey)),
16
+ rpcSchema: (0, viem_1.rpcSchema)(),
17
+ });
20
18
  }
21
19
  async getPaymasterData(rawUserOp, usePaymaster, safeNotDeployed) {
22
20
  // TODO: Keep this option out of the bundler
23
21
  if (usePaymaster) {
24
22
  console.log("Requesting paymaster data...");
25
- const data = this.provider.send("pm_sponsorUserOperation", [
26
- { ...rawUserOp, signature: util_1.PLACEHOLDER_SIG },
27
- this.entryPointAddress,
28
- ]);
29
- return data;
23
+ return handleRequest(() => this.client.request({
24
+ method: "pm_sponsorUserOperation",
25
+ params: [
26
+ { ...rawUserOp, signature: util_1.PLACEHOLDER_SIG },
27
+ this.entryPointAddress,
28
+ ],
29
+ }));
30
30
  }
31
31
  return defaultPaymasterData(safeNotDeployed);
32
32
  }
33
33
  async sendUserOperation(userOp) {
34
- try {
35
- const userOpHash = await this.provider.send("eth_sendUserOperation", [
36
- userOp,
37
- this.entryPointAddress,
38
- ]);
39
- return userOpHash;
40
- }
41
- catch (err) {
42
- const error = err.error;
43
- throw new Error(`Failed to send user op with: ${error.message}`);
44
- }
34
+ return handleRequest(() => this.client.request({
35
+ method: "eth_sendUserOperation",
36
+ params: [userOp, this.entryPointAddress],
37
+ }));
38
+ // throw new Error(`Failed to send user op with: ${error.message}`);
45
39
  }
46
40
  async getGasPrice() {
47
- return this.provider.send("pimlico_getUserOperationGasPrice", []);
48
- }
49
- async _getUserOpReceiptInner(userOpHash) {
50
- return this.provider.send("eth_getUserOperationReceipt", [userOpHash]);
41
+ return handleRequest(() => this.client.request({
42
+ method: "pimlico_getUserOperationGasPrice",
43
+ params: [],
44
+ }));
51
45
  }
52
46
  async getUserOpReceipt(userOpHash) {
53
47
  let userOpReceipt = null;
@@ -58,8 +52,33 @@ class Erc4337Bundler {
58
52
  }
59
53
  return userOpReceipt;
60
54
  }
55
+ async _getUserOpReceiptInner(userOpHash) {
56
+ return handleRequest(() => this.client.request({
57
+ method: "eth_getUserOperationReceipt",
58
+ params: [userOpHash],
59
+ }));
60
+ }
61
61
  }
62
62
  exports.Erc4337Bundler = Erc4337Bundler;
63
+ async function handleRequest(clientMethod) {
64
+ try {
65
+ return await clientMethod();
66
+ }
67
+ catch (error) {
68
+ if (error instanceof viem_1.HttpRequestError) {
69
+ if (error.status === 401) {
70
+ throw new Error("Unauthorized request. Please check your API key.");
71
+ }
72
+ else {
73
+ console.error(`Request failed with status ${error.status}: ${error.message}`);
74
+ }
75
+ }
76
+ else if (error instanceof viem_1.RpcError) {
77
+ throw new Error(`Failed to send user op with: ${error.message}`);
78
+ }
79
+ throw new Error(`Unexpected error ${error instanceof Error ? error.message : String(error)}`);
80
+ }
81
+ }
63
82
  // TODO(bh2smith) Should probably get reasonable estimates here:
64
83
  const defaultPaymasterData = (safeNotDeployed) => {
65
84
  return {
@@ -1,24 +1,27 @@
1
- import { ethers } from "ethers";
2
- import { Address, Hash, Hex } from "viem";
1
+ import { Address, Hash, Hex, ParseAbi, PublicClient } from "viem";
3
2
  import { GasPrice, MetaTransaction, UnsignedUserOperation, UserOperation } from "../types";
3
+ interface DeploymentData {
4
+ abi: unknown[] | ParseAbi<readonly string[]>;
5
+ address: `0x${string}`;
6
+ }
4
7
  /**
5
8
  * All contracts used in account creation & execution
6
9
  */
7
10
  export declare class ContractSuite {
8
- provider: ethers.JsonRpcProvider;
9
- singleton: ethers.Contract;
10
- proxyFactory: ethers.Contract;
11
- m4337: ethers.Contract;
12
- moduleSetup: ethers.Contract;
13
- entryPoint: ethers.Contract;
14
- constructor(provider: ethers.JsonRpcProvider, singleton: ethers.Contract, proxyFactory: ethers.Contract, m4337: ethers.Contract, moduleSetup: ethers.Contract, entryPoint: ethers.Contract);
11
+ dummyClient: PublicClient;
12
+ singleton: DeploymentData;
13
+ proxyFactory: DeploymentData;
14
+ m4337: DeploymentData;
15
+ moduleSetup: DeploymentData;
16
+ entryPoint: DeploymentData;
17
+ constructor(client: PublicClient, singleton: DeploymentData, proxyFactory: DeploymentData, m4337: DeploymentData, moduleSetup: DeploymentData, entryPoint: DeploymentData);
15
18
  static init(): Promise<ContractSuite>;
16
- addressForSetup(setup: ethers.BytesLike, saltNonce?: string): Promise<Address>;
17
- getSetup(owners: string[]): Promise<Hex>;
19
+ addressForSetup(setup: Hex, saltNonce?: string): Promise<Address>;
20
+ getSetup(owners: string[]): Hex;
21
+ addOwnerData(newOwner: Address): Hex;
18
22
  getOpHash(unsignedUserOp: UserOperation): Promise<Hash>;
19
- factoryDataForSetup(safeNotDeployed: boolean, setup: string, safeSaltNonce: string): {
20
- factory?: Address;
21
- factoryData?: Hex;
22
- };
23
- buildUserOp(txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
23
+ private factoryDataForSetup;
24
+ buildUserOp(nonce: bigint, txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
25
+ getNonce(address: Address, chainId: number): Promise<bigint>;
24
26
  }
27
+ export {};
@@ -3,14 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContractSuite = void 0;
4
4
  const safe_deployments_1 = require("@safe-global/safe-deployments");
5
5
  const safe_modules_deployments_1 = require("@safe-global/safe-modules-deployments");
6
- const ethers_1 = require("ethers");
6
+ const viem_1 = require("viem");
7
7
  const util_1 = require("../util");
8
8
  /**
9
9
  * All contracts used in account creation & execution
10
10
  */
11
11
  class ContractSuite {
12
- constructor(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint) {
13
- this.provider = provider;
12
+ constructor(client, singleton, proxyFactory, m4337, moduleSetup, entryPoint) {
13
+ this.dummyClient = client;
14
14
  this.singleton = singleton;
15
15
  this.proxyFactory = proxyFactory;
16
16
  this.m4337 = m4337;
@@ -19,109 +19,155 @@ class ContractSuite {
19
19
  }
20
20
  static async init() {
21
21
  // TODO - this is a cheeky hack.
22
- const provider = new ethers_1.ethers.JsonRpcProvider("https://rpc2.sepolia.org");
23
- const safeDeployment = (fn) => getDeployment(fn, { provider, version: "1.4.1" });
22
+ const client = (0, util_1.getClient)(11155111);
23
+ const safeDeployment = (fn) => getDeployment(fn, { version: "1.4.1" });
24
24
  const m4337Deployment = async (fn) => {
25
- return getDeployment(fn, { provider, version: "0.3.0" });
25
+ return getDeployment(fn, { version: "0.3.0" });
26
26
  };
27
- // Need this first to get entryPoint address
28
- const m4337 = await m4337Deployment(safe_modules_deployments_1.getSafe4337ModuleDeployment);
29
- const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = await Promise.all([
27
+ const [singleton, proxyFactory, moduleSetup, m4337] = await Promise.all([
30
28
  safeDeployment(safe_deployments_1.getSafeL2SingletonDeployment),
31
29
  safeDeployment(safe_deployments_1.getProxyFactoryDeployment),
32
30
  m4337Deployment(safe_modules_deployments_1.getSafeModuleSetupDeployment),
33
- m4337.SUPPORTED_ENTRYPOINT(),
31
+ m4337Deployment(safe_modules_deployments_1.getSafe4337ModuleDeployment),
34
32
  ]);
35
- const entryPoint = new ethers_1.ethers.Contract(supportedEntryPoint, ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], provider);
36
- console.log("Initialized ERC4337 & Safe Module Contracts:", {
37
- singleton: await singleton.getAddress(),
38
- proxyFactory: await proxyFactory.getAddress(),
39
- m4337: await m4337.getAddress(),
40
- moduleSetup: await moduleSetup.getAddress(),
41
- entryPoint: await entryPoint.getAddress(),
33
+ // console.log("Initialized ERC4337 & Safe Module Contracts:", {
34
+ // singleton: await singleton.getAddress(),
35
+ // proxyFactory: await proxyFactory.getAddress(),
36
+ // m4337: await m4337.getAddress(),
37
+ // moduleSetup: await moduleSetup.getAddress(),
38
+ // entryPoint: await entryPoint.getAddress(),
39
+ // });
40
+ return new ContractSuite(client, singleton, proxyFactory, m4337, moduleSetup,
41
+ // EntryPoint:
42
+ {
43
+ address: (await client.readContract({
44
+ address: m4337.address,
45
+ abi: m4337.abi,
46
+ functionName: "SUPPORTED_ENTRYPOINT",
47
+ })),
48
+ abi: (0, viem_1.parseAbi)([
49
+ "function getNonce(address, uint192 key) view returns (uint256 nonce)",
50
+ ]),
42
51
  });
43
- return new ContractSuite(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint);
44
52
  }
45
53
  async addressForSetup(setup, saltNonce) {
46
54
  // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
47
55
  // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58
48
- const salt = ethers_1.ethers.keccak256(ethers_1.ethers.solidityPacked(["bytes32", "uint256"], [ethers_1.ethers.keccak256(setup), saltNonce || 0]));
56
+ const salt = (0, viem_1.keccak256)((0, viem_1.encodePacked)(["bytes32", "uint256"], [(0, viem_1.keccak256)(setup), BigInt(saltNonce || "0")]));
49
57
  // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));
50
58
  // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29
51
- const initCode = ethers_1.ethers.solidityPacked(["bytes", "uint256"], [
52
- await this.proxyFactory.proxyCreationCode(),
53
- await this.singleton.getAddress(),
59
+ const initCode = (0, viem_1.encodePacked)(["bytes", "uint256"], [
60
+ (await this.dummyClient.readContract({
61
+ address: this.proxyFactory.address,
62
+ abi: this.proxyFactory.abi,
63
+ functionName: "proxyCreationCode",
64
+ })),
65
+ BigInt(this.singleton.address),
54
66
  ]);
55
- return ethers_1.ethers.getCreate2Address(await this.proxyFactory.getAddress(), salt, ethers_1.ethers.keccak256(initCode));
67
+ return (0, viem_1.getCreate2Address)({
68
+ from: this.proxyFactory.address,
69
+ salt,
70
+ bytecodeHash: (0, viem_1.keccak256)(initCode),
71
+ });
56
72
  }
57
- async getSetup(owners) {
58
- const setup = await this.singleton.interface.encodeFunctionData("setup", [
59
- owners,
60
- 1, // We use sign threshold of 1.
61
- this.moduleSetup.target,
62
- this.moduleSetup.interface.encodeFunctionData("enableModules", [
63
- [this.m4337.target],
64
- ]),
65
- this.m4337.target,
66
- ethers_1.ethers.ZeroAddress,
67
- 0,
68
- ethers_1.ethers.ZeroAddress,
69
- ]);
70
- return setup;
73
+ getSetup(owners) {
74
+ return (0, viem_1.encodeFunctionData)({
75
+ abi: this.singleton.abi,
76
+ functionName: "setup",
77
+ args: [
78
+ owners,
79
+ 1, // We use sign threshold of 1.
80
+ this.moduleSetup.address,
81
+ (0, viem_1.encodeFunctionData)({
82
+ abi: this.moduleSetup.abi,
83
+ functionName: "enableModules",
84
+ args: [[this.m4337.address]],
85
+ }),
86
+ this.m4337.address,
87
+ viem_1.zeroAddress,
88
+ 0,
89
+ viem_1.zeroAddress,
90
+ ],
91
+ });
92
+ }
93
+ addOwnerData(newOwner) {
94
+ return (0, viem_1.encodeFunctionData)({
95
+ abi: this.singleton.abi,
96
+ functionName: "addOwnerWithThreshold",
97
+ args: [newOwner, 1],
98
+ });
71
99
  }
72
100
  async getOpHash(unsignedUserOp) {
73
101
  const { factory, factoryData, verificationGasLimit, callGasLimit, maxPriorityFeePerGas, maxFeePerGas, } = unsignedUserOp;
74
- return this.m4337.getOperationHash({
75
- ...unsignedUserOp,
76
- initCode: factory
77
- ? ethers_1.ethers.solidityPacked(["address", "bytes"], [factory, factoryData])
78
- : "0x",
79
- accountGasLimits: (0, util_1.packGas)(verificationGasLimit, callGasLimit),
80
- gasFees: (0, util_1.packGas)(maxPriorityFeePerGas, maxFeePerGas),
81
- paymasterAndData: (0, util_1.packPaymasterData)(unsignedUserOp),
82
- signature: util_1.PLACEHOLDER_SIG,
102
+ const opHash = await this.dummyClient.readContract({
103
+ address: this.m4337.address,
104
+ abi: this.m4337.abi,
105
+ functionName: "getOperationHash",
106
+ args: [
107
+ {
108
+ ...unsignedUserOp,
109
+ initCode: factory
110
+ ? (0, viem_1.encodePacked)(["address", "bytes"], [factory, factoryData])
111
+ : "0x",
112
+ accountGasLimits: (0, util_1.packGas)(verificationGasLimit, callGasLimit),
113
+ gasFees: (0, util_1.packGas)(maxPriorityFeePerGas, maxFeePerGas),
114
+ paymasterAndData: (0, util_1.packPaymasterData)(unsignedUserOp),
115
+ signature: util_1.PLACEHOLDER_SIG,
116
+ },
117
+ ],
83
118
  });
119
+ return opHash;
84
120
  }
85
121
  factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce) {
86
122
  return safeNotDeployed
87
123
  ? {
88
- factory: this.proxyFactory.target,
89
- factoryData: this.proxyFactory.interface.encodeFunctionData("createProxyWithNonce", [this.singleton.target, setup, safeSaltNonce]),
124
+ factory: this.proxyFactory.address,
125
+ factoryData: (0, viem_1.encodeFunctionData)({
126
+ abi: this.proxyFactory.abi,
127
+ functionName: "createProxyWithNonce",
128
+ args: [this.singleton.address, setup, safeSaltNonce],
129
+ }),
90
130
  }
91
131
  : {};
92
132
  }
93
- async buildUserOp(txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
94
- const rawUserOp = {
133
+ async buildUserOp(nonce, txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
134
+ return {
95
135
  sender: safeAddress,
96
- nonce: ethers_1.ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)),
136
+ nonce: (0, viem_1.toHex)(nonce),
97
137
  ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce),
98
138
  // <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
99
- callData: this.m4337.interface.encodeFunctionData("executeUserOp", [
100
- txData.to,
101
- BigInt(txData.value),
102
- txData.data,
103
- txData.operation || 0,
104
- ]),
139
+ callData: (0, viem_1.encodeFunctionData)({
140
+ abi: this.m4337.abi,
141
+ functionName: "executeUserOp",
142
+ args: [
143
+ txData.to,
144
+ BigInt(txData.value),
145
+ txData.data,
146
+ txData.operation || 0,
147
+ ],
148
+ }),
105
149
  ...feeData,
106
150
  };
107
- return rawUserOp;
151
+ }
152
+ async getNonce(address, chainId) {
153
+ const nonce = (await (0, util_1.getClient)(chainId).readContract({
154
+ abi: this.entryPoint.abi,
155
+ address: this.entryPoint.address,
156
+ functionName: "getNonce",
157
+ args: [address, 0],
158
+ }));
159
+ return nonce;
108
160
  }
109
161
  }
110
162
  exports.ContractSuite = ContractSuite;
111
- async function getDeployment(fn, { provider, version }) {
112
- const { chainId } = await provider.getNetwork();
163
+ async function getDeployment(fn, { version }) {
113
164
  const deployment = fn({ version });
114
165
  if (!deployment) {
115
- throw new Error(`Deployment not found for ${fn.name} version ${version} on chainId ${chainId}`);
116
- }
117
- let address = deployment.networkAddresses[`${chainId}`];
118
- if (!address) {
119
- // console.warn(
120
- // `Deployment asset ${fn.name} not listed on chainId ${chainId}, using likely fallback. For more info visit https://github.com/safe-global/safe-modules-deployments`
121
- // );
122
- // TODO: This is a cheeky hack. Real solution proposed in
123
- // https://github.com/Mintbase/near-safe/issues/42
124
- address = deployment.networkAddresses["11155111"];
166
+ throw new Error(`Deployment not found for ${fn.name} version ${version}`);
125
167
  }
126
- return new ethers_1.ethers.Contract(address, deployment.abi, provider);
168
+ // TODO: maybe call parseAbi on deployment.abi here.
169
+ return {
170
+ address: deployment.networkAddresses["11155111"],
171
+ abi: deployment.abi,
172
+ };
127
173
  }
@@ -7,13 +7,12 @@ import { MetaTransaction, UserOperation, UserOperationReceipt } from "./types";
7
7
  export declare class TransactionManager {
8
8
  readonly nearAdapter: NearEthAdapter;
9
9
  readonly address: Address;
10
- readonly entryPointAddress: Address;
11
10
  private safePack;
12
11
  private setup;
13
12
  private pimlicoKey;
14
13
  private safeSaltNonce;
15
14
  private deployedChains;
16
- constructor(nearAdapter: NearEthAdapter, safePack: ContractSuite, pimlicoKey: string, setup: string, safeAddress: Address, entryPointAddress: Address, safeSaltNonce: string);
15
+ constructor(nearAdapter: NearEthAdapter, safePack: ContractSuite, pimlicoKey: string, setup: string, safeAddress: Address, safeSaltNonce: string);
17
16
  static create(config: {
18
17
  accountId: string;
19
18
  mpcContractId: string;
@@ -22,6 +21,7 @@ export declare class TransactionManager {
22
21
  safeSaltNonce?: string;
23
22
  }): Promise<TransactionManager>;
24
23
  get mpcAddress(): Address;
24
+ get mpcContractId(): string;
25
25
  getBalance(chainId: number): Promise<bigint>;
26
26
  bundlerForChainId(chainId: number): Erc4337Bundler;
27
27
  buildTransaction(args: {
@@ -34,7 +34,7 @@ export declare class TransactionManager {
34
34
  encodeSignRequest(tx: BaseTx): Promise<NearEthTxData>;
35
35
  executeTransaction(chainId: number, userOp: UserOperation): Promise<UserOperationReceipt>;
36
36
  safeDeployed(chainId: number): Promise<boolean>;
37
- addOwnerTx(address: string): MetaTransaction;
37
+ addOwnerTx(address: Address): MetaTransaction;
38
38
  safeSufficientlyFunded(chainId: number, transactions: MetaTransaction[], gasCost: bigint): Promise<boolean>;
39
39
  broadcastEvm(chainId: number, outcome: FinalExecutionOutcome, unsignedUserOp: UserOperation): Promise<{
40
40
  signature: Hex;
@@ -8,11 +8,10 @@ const multisend_1 = require("./lib/multisend");
8
8
  const safe_1 = require("./lib/safe");
9
9
  const util_1 = require("./util");
10
10
  class TransactionManager {
11
- constructor(nearAdapter, safePack, pimlicoKey, setup, safeAddress, entryPointAddress, safeSaltNonce) {
11
+ constructor(nearAdapter, safePack, pimlicoKey, setup, safeAddress, safeSaltNonce) {
12
12
  this.nearAdapter = nearAdapter;
13
13
  this.safePack = safePack;
14
14
  this.pimlicoKey = pimlicoKey;
15
- this.entryPointAddress = entryPointAddress;
16
15
  this.setup = setup;
17
16
  this.address = safeAddress;
18
17
  this.safeSaltNonce = safeSaltNonce;
@@ -25,34 +24,38 @@ class TransactionManager {
25
24
  safe_1.ContractSuite.init(),
26
25
  ]);
27
26
  console.log(`Near Adapter: ${nearAdapter.nearAccountId()} <> ${nearAdapter.address}`);
28
- const setup = await safePack.getSetup([nearAdapter.address]);
27
+ const setup = safePack.getSetup([nearAdapter.address]);
29
28
  const safeAddress = await safePack.addressForSetup(setup, config.safeSaltNonce);
30
- const entryPointAddress = (await safePack.entryPoint.getAddress());
31
29
  console.log(`Safe Address: ${safeAddress}`);
32
- return new TransactionManager(nearAdapter, safePack, pimlicoKey, setup, safeAddress, entryPointAddress, config.safeSaltNonce || "0");
30
+ return new TransactionManager(nearAdapter, safePack, pimlicoKey, setup, safeAddress, config.safeSaltNonce || "0");
33
31
  }
34
32
  get mpcAddress() {
35
33
  return this.nearAdapter.address;
36
34
  }
35
+ get mpcContractId() {
36
+ return this.nearAdapter.mpcContract.contract.contractId;
37
+ }
37
38
  async getBalance(chainId) {
38
- const provider = near_ca_1.Network.fromChainId(chainId).client;
39
- return await provider.getBalance({ address: this.address });
39
+ return await (0, util_1.getClient)(chainId).getBalance({ address: this.address });
40
40
  }
41
41
  bundlerForChainId(chainId) {
42
- return new bundler_1.Erc4337Bundler(this.entryPointAddress, this.pimlicoKey, chainId);
42
+ return new bundler_1.Erc4337Bundler(this.safePack.entryPoint.address, this.pimlicoKey, chainId);
43
43
  }
44
44
  async buildTransaction(args) {
45
45
  const { transactions, usePaymaster, chainId } = args;
46
- const bundler = this.bundlerForChainId(chainId);
47
- const gasFees = (await bundler.getGasPrice()).fast;
48
- // Build Singular MetaTransaction for Multisend from transaction list.
49
46
  if (transactions.length === 0) {
50
47
  throw new Error("Empty transaction set!");
51
48
  }
49
+ const bundler = this.bundlerForChainId(chainId);
50
+ const [gasFees, nonce, safeDeployed] = await Promise.all([
51
+ bundler.getGasPrice(),
52
+ this.safePack.getNonce(this.address, chainId),
53
+ this.safeDeployed(chainId),
54
+ ]);
55
+ // Build Singular MetaTransaction for Multisend from transaction list.
52
56
  const tx = transactions.length > 1 ? (0, multisend_1.encodeMulti)(transactions) : transactions[0];
53
- const safeNotDeployed = !(await this.safeDeployed(chainId));
54
- const rawUserOp = await this.safePack.buildUserOp(tx, this.address, gasFees, this.setup, safeNotDeployed, this.safeSaltNonce);
55
- const paymasterData = await bundler.getPaymasterData(rawUserOp, usePaymaster, safeNotDeployed);
57
+ const rawUserOp = await this.safePack.buildUserOp(nonce, tx, this.address, gasFees.fast, this.setup, !safeDeployed, this.safeSaltNonce);
58
+ const paymasterData = await bundler.getPaymasterData(rawUserOp, usePaymaster, !safeDeployed);
56
59
  const unsignedUserOp = { ...rawUserOp, ...paymasterData };
57
60
  return unsignedUserOp;
58
61
  }
@@ -97,11 +100,11 @@ class TransactionManager {
97
100
  return userOpReceipt;
98
101
  }
99
102
  async safeDeployed(chainId) {
103
+ // Early exit if already known.
100
104
  if (chainId in this.deployedChains) {
101
105
  return true;
102
106
  }
103
- const provider = near_ca_1.Network.fromChainId(chainId).client;
104
- const deployed = (await provider.getCode({ address: this.address })) !== "0x";
107
+ const deployed = await (0, util_1.isContract)(this.address, chainId);
105
108
  if (deployed) {
106
109
  this.deployedChains.add(chainId);
107
110
  }
@@ -111,7 +114,7 @@ class TransactionManager {
111
114
  return {
112
115
  to: this.address,
113
116
  value: "0",
114
- data: this.safePack.singleton.interface.encodeFunctionData("addOwnerWithThreshold", [address, 1]),
117
+ data: this.safePack.addOwnerData(address),
115
118
  };
116
119
  }
117
120
  async safeSufficientlyFunded(chainId, transactions, gasCost) {
@@ -1,4 +1,4 @@
1
- import { Hex } from "viem";
1
+ import { Address, Hex, PublicClient } from "viem";
2
2
  import { PaymasterData, MetaTransaction } from "./types";
3
3
  export declare const PLACEHOLDER_SIG: `0x${string}`;
4
4
  type IntLike = Hex | bigint | string | number;
@@ -6,4 +6,6 @@ export declare const packGas: (hi: IntLike, lo: IntLike) => string;
6
6
  export declare function packSignature(signature: `0x${string}`, validFrom?: number, validTo?: number): Hex;
7
7
  export declare function packPaymasterData(data: PaymasterData): Hex;
8
8
  export declare function containsValue(transactions: MetaTransaction[]): boolean;
9
+ export declare function isContract(address: Address, chainId: number): Promise<boolean>;
10
+ export declare function getClient(chainId: number): PublicClient;
9
11
  export {};
package/dist/cjs/util.js CHANGED
@@ -4,6 +4,9 @@ exports.packGas = exports.PLACEHOLDER_SIG = void 0;
4
4
  exports.packSignature = packSignature;
5
5
  exports.packPaymasterData = packPaymasterData;
6
6
  exports.containsValue = containsValue;
7
+ exports.isContract = isContract;
8
+ exports.getClient = getClient;
9
+ const near_ca_1 = require("near-ca");
7
10
  const viem_1 = require("viem");
8
11
  exports.PLACEHOLDER_SIG = (0, viem_1.encodePacked)(["uint48", "uint48"], [0, 0]);
9
12
  const packGas = (hi, lo) => (0, viem_1.encodePacked)(["uint128", "uint128"], [BigInt(hi), BigInt(lo)]);
@@ -24,3 +27,9 @@ function packPaymasterData(data) {
24
27
  function containsValue(transactions) {
25
28
  return transactions.some((tx) => tx.value !== "0");
26
29
  }
30
+ async function isContract(address, chainId) {
31
+ return (await getClient(chainId).getCode({ address })) !== undefined;
32
+ }
33
+ function getClient(chainId) {
34
+ return near_ca_1.Network.fromChainId(chainId).client;
35
+ }
@@ -1,15 +1,37 @@
1
- import { ethers } from "ethers";
1
+ import { Address, Hash, PublicClient, Transport } from "viem";
2
2
  import { GasPrices, PaymasterData, UnsignedUserOperation, UserOperation, UserOperationReceipt } from "../types";
3
+ type BundlerRpcSchema = [
4
+ {
5
+ Method: "pm_sponsorUserOperation";
6
+ Parameters: [UnsignedUserOperation, Address];
7
+ ReturnType: PaymasterData;
8
+ },
9
+ {
10
+ Method: "eth_sendUserOperation";
11
+ Parameters: [UserOperation, Address];
12
+ ReturnType: Hash;
13
+ },
14
+ {
15
+ Method: "pimlico_getUserOperationGasPrice";
16
+ Parameters: [];
17
+ ReturnType: GasPrices;
18
+ },
19
+ {
20
+ Method: "eth_getUserOperationReceipt";
21
+ Parameters: [Hash];
22
+ ReturnType: UserOperationReceipt | null;
23
+ }
24
+ ];
3
25
  export declare class Erc4337Bundler {
4
- provider: ethers.JsonRpcProvider;
5
- entryPointAddress: string;
26
+ client: PublicClient<Transport, undefined, undefined, BundlerRpcSchema>;
27
+ entryPointAddress: Address;
6
28
  apiKey: string;
7
29
  chainId: number;
8
- constructor(entryPointAddress: string, apiKey: string, chainId: number);
9
- client(chainId: number): ethers.JsonRpcProvider;
30
+ constructor(entryPointAddress: Address, apiKey: string, chainId: number);
10
31
  getPaymasterData(rawUserOp: UnsignedUserOperation, usePaymaster: boolean, safeNotDeployed: boolean): Promise<PaymasterData>;
11
- sendUserOperation(userOp: UserOperation): Promise<string>;
32
+ sendUserOperation(userOp: UserOperation): Promise<Hash>;
12
33
  getGasPrice(): Promise<GasPrices>;
13
- _getUserOpReceiptInner(userOpHash: string): Promise<UserOperationReceipt | null>;
14
- getUserOpReceipt(userOpHash: string): Promise<UserOperationReceipt>;
34
+ getUserOpReceipt(userOpHash: Hash): Promise<UserOperationReceipt>;
35
+ private _getUserOpReceiptInner;
15
36
  }
37
+ export {};
@@ -1,12 +1,10 @@
1
- // TODO: Ethers dependency is only for Generic HTTP Provider
2
- import { ethers } from "ethers";
3
- import { toHex } from "viem";
1
+ import { createPublicClient, http, rpcSchema, toHex, RpcError, HttpRequestError, } from "viem";
4
2
  import { PLACEHOLDER_SIG } from "../util";
5
3
  function bundlerUrl(chainId, apikey) {
6
4
  return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${apikey}`;
7
5
  }
8
6
  export class Erc4337Bundler {
9
- provider;
7
+ client;
10
8
  entryPointAddress;
11
9
  apiKey;
12
10
  chainId;
@@ -14,41 +12,37 @@ export class Erc4337Bundler {
14
12
  this.entryPointAddress = entryPointAddress;
15
13
  this.apiKey = apiKey;
16
14
  this.chainId = chainId;
17
- this.provider = new ethers.JsonRpcProvider(bundlerUrl(chainId, apiKey));
18
- }
19
- client(chainId) {
20
- return new ethers.JsonRpcProvider(bundlerUrl(chainId, this.apiKey));
15
+ this.client = createPublicClient({
16
+ transport: http(bundlerUrl(chainId, this.apiKey)),
17
+ rpcSchema: rpcSchema(),
18
+ });
21
19
  }
22
20
  async getPaymasterData(rawUserOp, usePaymaster, safeNotDeployed) {
23
21
  // TODO: Keep this option out of the bundler
24
22
  if (usePaymaster) {
25
23
  console.log("Requesting paymaster data...");
26
- const data = this.provider.send("pm_sponsorUserOperation", [
27
- { ...rawUserOp, signature: PLACEHOLDER_SIG },
28
- this.entryPointAddress,
29
- ]);
30
- return data;
24
+ return handleRequest(() => this.client.request({
25
+ method: "pm_sponsorUserOperation",
26
+ params: [
27
+ { ...rawUserOp, signature: PLACEHOLDER_SIG },
28
+ this.entryPointAddress,
29
+ ],
30
+ }));
31
31
  }
32
32
  return defaultPaymasterData(safeNotDeployed);
33
33
  }
34
34
  async sendUserOperation(userOp) {
35
- try {
36
- const userOpHash = await this.provider.send("eth_sendUserOperation", [
37
- userOp,
38
- this.entryPointAddress,
39
- ]);
40
- return userOpHash;
41
- }
42
- catch (err) {
43
- const error = err.error;
44
- throw new Error(`Failed to send user op with: ${error.message}`);
45
- }
35
+ return handleRequest(() => this.client.request({
36
+ method: "eth_sendUserOperation",
37
+ params: [userOp, this.entryPointAddress],
38
+ }));
39
+ // throw new Error(`Failed to send user op with: ${error.message}`);
46
40
  }
47
41
  async getGasPrice() {
48
- return this.provider.send("pimlico_getUserOperationGasPrice", []);
49
- }
50
- async _getUserOpReceiptInner(userOpHash) {
51
- return this.provider.send("eth_getUserOperationReceipt", [userOpHash]);
42
+ return handleRequest(() => this.client.request({
43
+ method: "pimlico_getUserOperationGasPrice",
44
+ params: [],
45
+ }));
52
46
  }
53
47
  async getUserOpReceipt(userOpHash) {
54
48
  let userOpReceipt = null;
@@ -59,6 +53,31 @@ export class Erc4337Bundler {
59
53
  }
60
54
  return userOpReceipt;
61
55
  }
56
+ async _getUserOpReceiptInner(userOpHash) {
57
+ return handleRequest(() => this.client.request({
58
+ method: "eth_getUserOperationReceipt",
59
+ params: [userOpHash],
60
+ }));
61
+ }
62
+ }
63
+ async function handleRequest(clientMethod) {
64
+ try {
65
+ return await clientMethod();
66
+ }
67
+ catch (error) {
68
+ if (error instanceof HttpRequestError) {
69
+ if (error.status === 401) {
70
+ throw new Error("Unauthorized request. Please check your API key.");
71
+ }
72
+ else {
73
+ console.error(`Request failed with status ${error.status}: ${error.message}`);
74
+ }
75
+ }
76
+ else if (error instanceof RpcError) {
77
+ throw new Error(`Failed to send user op with: ${error.message}`);
78
+ }
79
+ throw new Error(`Unexpected error ${error instanceof Error ? error.message : String(error)}`);
80
+ }
62
81
  }
63
82
  // TODO(bh2smith) Should probably get reasonable estimates here:
64
83
  const defaultPaymasterData = (safeNotDeployed) => {
@@ -1,24 +1,27 @@
1
- import { ethers } from "ethers";
2
- import { Address, Hash, Hex } from "viem";
1
+ import { Address, Hash, Hex, ParseAbi, PublicClient } from "viem";
3
2
  import { GasPrice, MetaTransaction, UnsignedUserOperation, UserOperation } from "../types";
3
+ interface DeploymentData {
4
+ abi: unknown[] | ParseAbi<readonly string[]>;
5
+ address: `0x${string}`;
6
+ }
4
7
  /**
5
8
  * All contracts used in account creation & execution
6
9
  */
7
10
  export declare class ContractSuite {
8
- provider: ethers.JsonRpcProvider;
9
- singleton: ethers.Contract;
10
- proxyFactory: ethers.Contract;
11
- m4337: ethers.Contract;
12
- moduleSetup: ethers.Contract;
13
- entryPoint: ethers.Contract;
14
- constructor(provider: ethers.JsonRpcProvider, singleton: ethers.Contract, proxyFactory: ethers.Contract, m4337: ethers.Contract, moduleSetup: ethers.Contract, entryPoint: ethers.Contract);
11
+ dummyClient: PublicClient;
12
+ singleton: DeploymentData;
13
+ proxyFactory: DeploymentData;
14
+ m4337: DeploymentData;
15
+ moduleSetup: DeploymentData;
16
+ entryPoint: DeploymentData;
17
+ constructor(client: PublicClient, singleton: DeploymentData, proxyFactory: DeploymentData, m4337: DeploymentData, moduleSetup: DeploymentData, entryPoint: DeploymentData);
15
18
  static init(): Promise<ContractSuite>;
16
- addressForSetup(setup: ethers.BytesLike, saltNonce?: string): Promise<Address>;
17
- getSetup(owners: string[]): Promise<Hex>;
19
+ addressForSetup(setup: Hex, saltNonce?: string): Promise<Address>;
20
+ getSetup(owners: string[]): Hex;
21
+ addOwnerData(newOwner: Address): Hex;
18
22
  getOpHash(unsignedUserOp: UserOperation): Promise<Hash>;
19
- factoryDataForSetup(safeNotDeployed: boolean, setup: string, safeSaltNonce: string): {
20
- factory?: Address;
21
- factoryData?: Hex;
22
- };
23
- buildUserOp(txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
23
+ private factoryDataForSetup;
24
+ buildUserOp(nonce: bigint, txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
25
+ getNonce(address: Address, chainId: number): Promise<bigint>;
24
26
  }
27
+ export {};
@@ -1,19 +1,20 @@
1
1
  import { getProxyFactoryDeployment, getSafeL2SingletonDeployment, } from "@safe-global/safe-deployments";
2
2
  import { getSafe4337ModuleDeployment, getSafeModuleSetupDeployment, } from "@safe-global/safe-modules-deployments";
3
- import { ethers } from "ethers";
4
- import { PLACEHOLDER_SIG, packGas, packPaymasterData } from "../util";
3
+ import { encodeFunctionData, encodePacked, getCreate2Address, keccak256, parseAbi, toHex, zeroAddress, } from "viem";
4
+ import { PLACEHOLDER_SIG, getClient, packGas, packPaymasterData, } from "../util";
5
5
  /**
6
6
  * All contracts used in account creation & execution
7
7
  */
8
8
  export class ContractSuite {
9
- provider;
9
+ // Used only for stateless contract reads.
10
+ dummyClient;
10
11
  singleton;
11
12
  proxyFactory;
12
13
  m4337;
13
14
  moduleSetup;
14
15
  entryPoint;
15
- constructor(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint) {
16
- this.provider = provider;
16
+ constructor(client, singleton, proxyFactory, m4337, moduleSetup, entryPoint) {
17
+ this.dummyClient = client;
17
18
  this.singleton = singleton;
18
19
  this.proxyFactory = proxyFactory;
19
20
  this.m4337 = m4337;
@@ -22,108 +23,154 @@ export class ContractSuite {
22
23
  }
23
24
  static async init() {
24
25
  // TODO - this is a cheeky hack.
25
- const provider = new ethers.JsonRpcProvider("https://rpc2.sepolia.org");
26
- const safeDeployment = (fn) => getDeployment(fn, { provider, version: "1.4.1" });
26
+ const client = getClient(11155111);
27
+ const safeDeployment = (fn) => getDeployment(fn, { version: "1.4.1" });
27
28
  const m4337Deployment = async (fn) => {
28
- return getDeployment(fn, { provider, version: "0.3.0" });
29
+ return getDeployment(fn, { version: "0.3.0" });
29
30
  };
30
- // Need this first to get entryPoint address
31
- const m4337 = await m4337Deployment(getSafe4337ModuleDeployment);
32
- const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = await Promise.all([
31
+ const [singleton, proxyFactory, moduleSetup, m4337] = await Promise.all([
33
32
  safeDeployment(getSafeL2SingletonDeployment),
34
33
  safeDeployment(getProxyFactoryDeployment),
35
34
  m4337Deployment(getSafeModuleSetupDeployment),
36
- m4337.SUPPORTED_ENTRYPOINT(),
35
+ m4337Deployment(getSafe4337ModuleDeployment),
37
36
  ]);
38
- const entryPoint = new ethers.Contract(supportedEntryPoint, ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], provider);
39
- console.log("Initialized ERC4337 & Safe Module Contracts:", {
40
- singleton: await singleton.getAddress(),
41
- proxyFactory: await proxyFactory.getAddress(),
42
- m4337: await m4337.getAddress(),
43
- moduleSetup: await moduleSetup.getAddress(),
44
- entryPoint: await entryPoint.getAddress(),
37
+ // console.log("Initialized ERC4337 & Safe Module Contracts:", {
38
+ // singleton: await singleton.getAddress(),
39
+ // proxyFactory: await proxyFactory.getAddress(),
40
+ // m4337: await m4337.getAddress(),
41
+ // moduleSetup: await moduleSetup.getAddress(),
42
+ // entryPoint: await entryPoint.getAddress(),
43
+ // });
44
+ return new ContractSuite(client, singleton, proxyFactory, m4337, moduleSetup,
45
+ // EntryPoint:
46
+ {
47
+ address: (await client.readContract({
48
+ address: m4337.address,
49
+ abi: m4337.abi,
50
+ functionName: "SUPPORTED_ENTRYPOINT",
51
+ })),
52
+ abi: parseAbi([
53
+ "function getNonce(address, uint192 key) view returns (uint256 nonce)",
54
+ ]),
45
55
  });
46
- return new ContractSuite(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint);
47
56
  }
48
57
  async addressForSetup(setup, saltNonce) {
49
58
  // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
50
59
  // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58
51
- const salt = ethers.keccak256(ethers.solidityPacked(["bytes32", "uint256"], [ethers.keccak256(setup), saltNonce || 0]));
60
+ const salt = keccak256(encodePacked(["bytes32", "uint256"], [keccak256(setup), BigInt(saltNonce || "0")]));
52
61
  // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));
53
62
  // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29
54
- const initCode = ethers.solidityPacked(["bytes", "uint256"], [
55
- await this.proxyFactory.proxyCreationCode(),
56
- await this.singleton.getAddress(),
63
+ const initCode = encodePacked(["bytes", "uint256"], [
64
+ (await this.dummyClient.readContract({
65
+ address: this.proxyFactory.address,
66
+ abi: this.proxyFactory.abi,
67
+ functionName: "proxyCreationCode",
68
+ })),
69
+ BigInt(this.singleton.address),
57
70
  ]);
58
- return ethers.getCreate2Address(await this.proxyFactory.getAddress(), salt, ethers.keccak256(initCode));
71
+ return getCreate2Address({
72
+ from: this.proxyFactory.address,
73
+ salt,
74
+ bytecodeHash: keccak256(initCode),
75
+ });
59
76
  }
60
- async getSetup(owners) {
61
- const setup = await this.singleton.interface.encodeFunctionData("setup", [
62
- owners,
63
- 1, // We use sign threshold of 1.
64
- this.moduleSetup.target,
65
- this.moduleSetup.interface.encodeFunctionData("enableModules", [
66
- [this.m4337.target],
67
- ]),
68
- this.m4337.target,
69
- ethers.ZeroAddress,
70
- 0,
71
- ethers.ZeroAddress,
72
- ]);
73
- return setup;
77
+ getSetup(owners) {
78
+ return encodeFunctionData({
79
+ abi: this.singleton.abi,
80
+ functionName: "setup",
81
+ args: [
82
+ owners,
83
+ 1, // We use sign threshold of 1.
84
+ this.moduleSetup.address,
85
+ encodeFunctionData({
86
+ abi: this.moduleSetup.abi,
87
+ functionName: "enableModules",
88
+ args: [[this.m4337.address]],
89
+ }),
90
+ this.m4337.address,
91
+ zeroAddress,
92
+ 0,
93
+ zeroAddress,
94
+ ],
95
+ });
96
+ }
97
+ addOwnerData(newOwner) {
98
+ return encodeFunctionData({
99
+ abi: this.singleton.abi,
100
+ functionName: "addOwnerWithThreshold",
101
+ args: [newOwner, 1],
102
+ });
74
103
  }
75
104
  async getOpHash(unsignedUserOp) {
76
105
  const { factory, factoryData, verificationGasLimit, callGasLimit, maxPriorityFeePerGas, maxFeePerGas, } = unsignedUserOp;
77
- return this.m4337.getOperationHash({
78
- ...unsignedUserOp,
79
- initCode: factory
80
- ? ethers.solidityPacked(["address", "bytes"], [factory, factoryData])
81
- : "0x",
82
- accountGasLimits: packGas(verificationGasLimit, callGasLimit),
83
- gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas),
84
- paymasterAndData: packPaymasterData(unsignedUserOp),
85
- signature: PLACEHOLDER_SIG,
106
+ const opHash = await this.dummyClient.readContract({
107
+ address: this.m4337.address,
108
+ abi: this.m4337.abi,
109
+ functionName: "getOperationHash",
110
+ args: [
111
+ {
112
+ ...unsignedUserOp,
113
+ initCode: factory
114
+ ? encodePacked(["address", "bytes"], [factory, factoryData])
115
+ : "0x",
116
+ accountGasLimits: packGas(verificationGasLimit, callGasLimit),
117
+ gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas),
118
+ paymasterAndData: packPaymasterData(unsignedUserOp),
119
+ signature: PLACEHOLDER_SIG,
120
+ },
121
+ ],
86
122
  });
123
+ return opHash;
87
124
  }
88
125
  factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce) {
89
126
  return safeNotDeployed
90
127
  ? {
91
- factory: this.proxyFactory.target,
92
- factoryData: this.proxyFactory.interface.encodeFunctionData("createProxyWithNonce", [this.singleton.target, setup, safeSaltNonce]),
128
+ factory: this.proxyFactory.address,
129
+ factoryData: encodeFunctionData({
130
+ abi: this.proxyFactory.abi,
131
+ functionName: "createProxyWithNonce",
132
+ args: [this.singleton.address, setup, safeSaltNonce],
133
+ }),
93
134
  }
94
135
  : {};
95
136
  }
96
- async buildUserOp(txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
97
- const rawUserOp = {
137
+ async buildUserOp(nonce, txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
138
+ return {
98
139
  sender: safeAddress,
99
- nonce: ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)),
140
+ nonce: toHex(nonce),
100
141
  ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce),
101
142
  // <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
102
- callData: this.m4337.interface.encodeFunctionData("executeUserOp", [
103
- txData.to,
104
- BigInt(txData.value),
105
- txData.data,
106
- txData.operation || 0,
107
- ]),
143
+ callData: encodeFunctionData({
144
+ abi: this.m4337.abi,
145
+ functionName: "executeUserOp",
146
+ args: [
147
+ txData.to,
148
+ BigInt(txData.value),
149
+ txData.data,
150
+ txData.operation || 0,
151
+ ],
152
+ }),
108
153
  ...feeData,
109
154
  };
110
- return rawUserOp;
155
+ }
156
+ async getNonce(address, chainId) {
157
+ const nonce = (await getClient(chainId).readContract({
158
+ abi: this.entryPoint.abi,
159
+ address: this.entryPoint.address,
160
+ functionName: "getNonce",
161
+ args: [address, 0],
162
+ }));
163
+ return nonce;
111
164
  }
112
165
  }
113
- async function getDeployment(fn, { provider, version }) {
114
- const { chainId } = await provider.getNetwork();
166
+ async function getDeployment(fn, { version }) {
115
167
  const deployment = fn({ version });
116
168
  if (!deployment) {
117
- throw new Error(`Deployment not found for ${fn.name} version ${version} on chainId ${chainId}`);
118
- }
119
- let address = deployment.networkAddresses[`${chainId}`];
120
- if (!address) {
121
- // console.warn(
122
- // `Deployment asset ${fn.name} not listed on chainId ${chainId}, using likely fallback. For more info visit https://github.com/safe-global/safe-modules-deployments`
123
- // );
124
- // TODO: This is a cheeky hack. Real solution proposed in
125
- // https://github.com/Mintbase/near-safe/issues/42
126
- address = deployment.networkAddresses["11155111"];
169
+ throw new Error(`Deployment not found for ${fn.name} version ${version}`);
127
170
  }
128
- return new ethers.Contract(address, deployment.abi, provider);
171
+ // TODO: maybe call parseAbi on deployment.abi here.
172
+ return {
173
+ address: deployment.networkAddresses["11155111"],
174
+ abi: deployment.abi,
175
+ };
129
176
  }
@@ -7,13 +7,12 @@ import { MetaTransaction, UserOperation, UserOperationReceipt } from "./types";
7
7
  export declare class TransactionManager {
8
8
  readonly nearAdapter: NearEthAdapter;
9
9
  readonly address: Address;
10
- readonly entryPointAddress: Address;
11
10
  private safePack;
12
11
  private setup;
13
12
  private pimlicoKey;
14
13
  private safeSaltNonce;
15
14
  private deployedChains;
16
- constructor(nearAdapter: NearEthAdapter, safePack: ContractSuite, pimlicoKey: string, setup: string, safeAddress: Address, entryPointAddress: Address, safeSaltNonce: string);
15
+ constructor(nearAdapter: NearEthAdapter, safePack: ContractSuite, pimlicoKey: string, setup: string, safeAddress: Address, safeSaltNonce: string);
17
16
  static create(config: {
18
17
  accountId: string;
19
18
  mpcContractId: string;
@@ -22,6 +21,7 @@ export declare class TransactionManager {
22
21
  safeSaltNonce?: string;
23
22
  }): Promise<TransactionManager>;
24
23
  get mpcAddress(): Address;
24
+ get mpcContractId(): string;
25
25
  getBalance(chainId: number): Promise<bigint>;
26
26
  bundlerForChainId(chainId: number): Erc4337Bundler;
27
27
  buildTransaction(args: {
@@ -34,7 +34,7 @@ export declare class TransactionManager {
34
34
  encodeSignRequest(tx: BaseTx): Promise<NearEthTxData>;
35
35
  executeTransaction(chainId: number, userOp: UserOperation): Promise<UserOperationReceipt>;
36
36
  safeDeployed(chainId: number): Promise<boolean>;
37
- addOwnerTx(address: string): MetaTransaction;
37
+ addOwnerTx(address: Address): MetaTransaction;
38
38
  safeSufficientlyFunded(chainId: number, transactions: MetaTransaction[], gasCost: bigint): Promise<boolean>;
39
39
  broadcastEvm(chainId: number, outcome: FinalExecutionOutcome, unsignedUserOp: UserOperation): Promise<{
40
40
  signature: Hex;
@@ -1,23 +1,21 @@
1
- import { Network, setupAdapter, signatureFromOutcome, } from "near-ca";
1
+ import { setupAdapter, signatureFromOutcome, } from "near-ca";
2
2
  import { serializeSignature } from "viem";
3
3
  import { Erc4337Bundler } from "./lib/bundler";
4
4
  import { encodeMulti } from "./lib/multisend";
5
5
  import { ContractSuite } from "./lib/safe";
6
- import { packSignature } from "./util";
6
+ import { getClient, isContract, packSignature } from "./util";
7
7
  export class TransactionManager {
8
8
  nearAdapter;
9
9
  address;
10
- entryPointAddress;
11
10
  safePack;
12
11
  setup;
13
12
  pimlicoKey;
14
13
  safeSaltNonce;
15
14
  deployedChains;
16
- constructor(nearAdapter, safePack, pimlicoKey, setup, safeAddress, entryPointAddress, safeSaltNonce) {
15
+ constructor(nearAdapter, safePack, pimlicoKey, setup, safeAddress, safeSaltNonce) {
17
16
  this.nearAdapter = nearAdapter;
18
17
  this.safePack = safePack;
19
18
  this.pimlicoKey = pimlicoKey;
20
- this.entryPointAddress = entryPointAddress;
21
19
  this.setup = setup;
22
20
  this.address = safeAddress;
23
21
  this.safeSaltNonce = safeSaltNonce;
@@ -30,34 +28,38 @@ export class TransactionManager {
30
28
  ContractSuite.init(),
31
29
  ]);
32
30
  console.log(`Near Adapter: ${nearAdapter.nearAccountId()} <> ${nearAdapter.address}`);
33
- const setup = await safePack.getSetup([nearAdapter.address]);
31
+ const setup = safePack.getSetup([nearAdapter.address]);
34
32
  const safeAddress = await safePack.addressForSetup(setup, config.safeSaltNonce);
35
- const entryPointAddress = (await safePack.entryPoint.getAddress());
36
33
  console.log(`Safe Address: ${safeAddress}`);
37
- return new TransactionManager(nearAdapter, safePack, pimlicoKey, setup, safeAddress, entryPointAddress, config.safeSaltNonce || "0");
34
+ return new TransactionManager(nearAdapter, safePack, pimlicoKey, setup, safeAddress, config.safeSaltNonce || "0");
38
35
  }
39
36
  get mpcAddress() {
40
37
  return this.nearAdapter.address;
41
38
  }
39
+ get mpcContractId() {
40
+ return this.nearAdapter.mpcContract.contract.contractId;
41
+ }
42
42
  async getBalance(chainId) {
43
- const provider = Network.fromChainId(chainId).client;
44
- return await provider.getBalance({ address: this.address });
43
+ return await getClient(chainId).getBalance({ address: this.address });
45
44
  }
46
45
  bundlerForChainId(chainId) {
47
- return new Erc4337Bundler(this.entryPointAddress, this.pimlicoKey, chainId);
46
+ return new Erc4337Bundler(this.safePack.entryPoint.address, this.pimlicoKey, chainId);
48
47
  }
49
48
  async buildTransaction(args) {
50
49
  const { transactions, usePaymaster, chainId } = args;
51
- const bundler = this.bundlerForChainId(chainId);
52
- const gasFees = (await bundler.getGasPrice()).fast;
53
- // Build Singular MetaTransaction for Multisend from transaction list.
54
50
  if (transactions.length === 0) {
55
51
  throw new Error("Empty transaction set!");
56
52
  }
53
+ const bundler = this.bundlerForChainId(chainId);
54
+ const [gasFees, nonce, safeDeployed] = await Promise.all([
55
+ bundler.getGasPrice(),
56
+ this.safePack.getNonce(this.address, chainId),
57
+ this.safeDeployed(chainId),
58
+ ]);
59
+ // Build Singular MetaTransaction for Multisend from transaction list.
57
60
  const tx = transactions.length > 1 ? encodeMulti(transactions) : transactions[0];
58
- const safeNotDeployed = !(await this.safeDeployed(chainId));
59
- const rawUserOp = await this.safePack.buildUserOp(tx, this.address, gasFees, this.setup, safeNotDeployed, this.safeSaltNonce);
60
- const paymasterData = await bundler.getPaymasterData(rawUserOp, usePaymaster, safeNotDeployed);
61
+ const rawUserOp = await this.safePack.buildUserOp(nonce, tx, this.address, gasFees.fast, this.setup, !safeDeployed, this.safeSaltNonce);
62
+ const paymasterData = await bundler.getPaymasterData(rawUserOp, usePaymaster, !safeDeployed);
61
63
  const unsignedUserOp = { ...rawUserOp, ...paymasterData };
62
64
  return unsignedUserOp;
63
65
  }
@@ -102,11 +104,11 @@ export class TransactionManager {
102
104
  return userOpReceipt;
103
105
  }
104
106
  async safeDeployed(chainId) {
107
+ // Early exit if already known.
105
108
  if (chainId in this.deployedChains) {
106
109
  return true;
107
110
  }
108
- const provider = Network.fromChainId(chainId).client;
109
- const deployed = (await provider.getCode({ address: this.address })) !== "0x";
111
+ const deployed = await isContract(this.address, chainId);
110
112
  if (deployed) {
111
113
  this.deployedChains.add(chainId);
112
114
  }
@@ -116,7 +118,7 @@ export class TransactionManager {
116
118
  return {
117
119
  to: this.address,
118
120
  value: "0",
119
- data: this.safePack.singleton.interface.encodeFunctionData("addOwnerWithThreshold", [address, 1]),
121
+ data: this.safePack.addOwnerData(address),
120
122
  };
121
123
  }
122
124
  async safeSufficientlyFunded(chainId, transactions, gasCost) {
@@ -1,4 +1,4 @@
1
- import { Hex } from "viem";
1
+ import { Address, Hex, PublicClient } from "viem";
2
2
  import { PaymasterData, MetaTransaction } from "./types";
3
3
  export declare const PLACEHOLDER_SIG: `0x${string}`;
4
4
  type IntLike = Hex | bigint | string | number;
@@ -6,4 +6,6 @@ export declare const packGas: (hi: IntLike, lo: IntLike) => string;
6
6
  export declare function packSignature(signature: `0x${string}`, validFrom?: number, validTo?: number): Hex;
7
7
  export declare function packPaymasterData(data: PaymasterData): Hex;
8
8
  export declare function containsValue(transactions: MetaTransaction[]): boolean;
9
+ export declare function isContract(address: Address, chainId: number): Promise<boolean>;
10
+ export declare function getClient(chainId: number): PublicClient;
9
11
  export {};
package/dist/esm/util.js CHANGED
@@ -1,4 +1,5 @@
1
- import { concatHex, encodePacked, toHex } from "viem";
1
+ import { Network } from "near-ca";
2
+ import { concatHex, encodePacked, toHex, } from "viem";
2
3
  export const PLACEHOLDER_SIG = encodePacked(["uint48", "uint48"], [0, 0]);
3
4
  export const packGas = (hi, lo) => encodePacked(["uint128", "uint128"], [BigInt(hi), BigInt(lo)]);
4
5
  export function packSignature(signature, validFrom = 0, validTo = 0) {
@@ -17,3 +18,9 @@ export function packPaymasterData(data) {
17
18
  export function containsValue(transactions) {
18
19
  return transactions.some((tx) => tx.value !== "0");
19
20
  }
21
+ export async function isContract(address, chainId) {
22
+ return (await getClient(chainId).getCode({ address })) !== undefined;
23
+ }
24
+ export function getClient(chainId) {
25
+ return Network.fromChainId(chainId).client;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "near-safe",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "An SDK for controlling Ethereum Smart Accounts via ERC4337 from a Near Account.",
6
6
  "author": "bh2smith",
@@ -42,11 +42,9 @@
42
42
  "dependencies": {
43
43
  "@safe-global/safe-deployments": "^1.37.0",
44
44
  "@safe-global/safe-modules-deployments": "^2.2.0",
45
- "ethers": "^6.13.1",
46
45
  "near-api-js": "^5.0.0",
47
46
  "near-ca": "^0.5.2",
48
- "viem": "^2.16.5",
49
- "yargs": "^17.7.2"
47
+ "viem": "^2.16.5"
50
48
  },
51
49
  "devDependencies": {
52
50
  "@types/jest": "^29.5.12",
@@ -57,11 +55,16 @@
57
55
  "dotenv": "^16.4.5",
58
56
  "eslint": "^9.6.0",
59
57
  "eslint-plugin-import": "^2.30.0",
58
+ "ethers": "^6.13.1",
60
59
  "jest": "^29.7.0",
61
60
  "prettier": "^3.3.2",
62
61
  "ts-jest": "^29.1.5",
63
62
  "tsx": "^4.16.0",
64
63
  "typescript": "^5.5.2",
65
64
  "yargs": "^17.7.2"
65
+ },
66
+ "resolutions": {
67
+ "glob": "^9.0.0",
68
+ "base-x": "^3.0.0"
66
69
  }
67
70
  }