near-safe 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Near-Safe
2
+
3
+ **TLDR;** This project provides a TypeScript implementation for controling Ethereum Smart Accounts via ERC4337 standard from a Near Account.
4
+ It includes utilities for packing data, managing transactions, and interacting with a bundler.
5
+
6
+ The account structure is defined as follows:
7
+
8
+ 1. **Near Account** produces signatures for a deterministic EOA via Near's MPC Contract for [Chain Signatures](https://docs.near.org/concepts/abstraction/chain-signatures)
9
+ 2. This EOA (controled by the Near Account) is the owner of a deterministic [Safe](https://safe.global/) with configured support for [ERC4337](https://www.erc4337.io/) standard.
10
+
11
+ ## Features
12
+
13
+ 1. Users first transaction is bundled together with the Safe's deployement (i.e. Safe does not need to be created before it is used). This is achived as multisend transaction.
14
+ 2. No need to fund the EOA Account (it is only used for signatures).
15
+ 3. Account Recovery: Near's MPC service provides signatures for accounts that users control, but do not hold the private key for. Provide a "recoveryAddress" that will be added as an additional owner of the Safe.
16
+ 4. Paymaster Support for an entirely gasless experience!
17
+ 5. Same address on all chains!
18
+
19
+ ## Installation & Configuration
20
+
21
+ To get started, clone the repository and install the dependencies:
22
+
23
+ ```sh
24
+ yarn install
25
+ ```
26
+
27
+ Create a `.env` (or use our `.env.sample`) file in the root of the project and add the following environment variables:
28
+
29
+ ```sh
30
+ ETH_RPC=https://rpc2.sepolia.org
31
+
32
+ NEAR_ACCOUNT_ID=
33
+ NEAR_ACCOUNT_PRIVATE_KEY=
34
+
35
+ # Head to https://www.pimlico.io/ for an API key
36
+ ERC4337_BUNDLER_URL=
37
+ ```
38
+
39
+
40
+ ## Usage
41
+
42
+ ### Running the Example
43
+
44
+ The example script `examples/send-tx.ts` demonstrates how to use the transaction manager. You can run it with the following command:
45
+
46
+ ```sh
47
+ yarn start --usePaymaster --recoveryAddress <recovery_address> --safeSaltNonce <safe_salt_nonce>
48
+ ```
49
+
50
+ ### Example Arguments
51
+
52
+ The example script accepts the following arguments:
53
+
54
+ - `--usePaymaster`: Boolean flag to indicate if the transaction should be sponsored by a paymaster service.
55
+ - `--recoveryAddress`: The recovery address to be attached as the owner of the Safe (immediately after deployment).
56
+ - `--safeSaltNonce`: The salt nonce used for the Safe deployment.
57
+
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./tx-manager"), exports);
18
+ __exportStar(require("./types"), exports);
19
+ __exportStar(require("./util"), exports);
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Erc4337Bundler = void 0;
4
+ const ethers_1 = require("ethers");
5
+ const util_1 = require("../util");
6
+ class Erc4337Bundler {
7
+ constructor(bundlerUrl, entryPointAddress) {
8
+ this.entryPointAddress = entryPointAddress;
9
+ this.provider = new ethers_1.ethers.JsonRpcProvider(bundlerUrl);
10
+ }
11
+ async getPaymasterData(rawUserOp, usePaymaster, safeNotDeployed) {
12
+ // TODO: Keep this option out of the bundler
13
+ if (usePaymaster) {
14
+ console.log("Requesting paymaster data...");
15
+ const data = this.provider.send("pm_sponsorUserOperation", [
16
+ { ...rawUserOp, signature: util_1.PLACEHOLDER_SIG },
17
+ this.entryPointAddress,
18
+ ]);
19
+ return data;
20
+ }
21
+ return defaultPaymasterData(safeNotDeployed);
22
+ }
23
+ async sendUserOperation(userOp) {
24
+ try {
25
+ const userOpHash = await this.provider.send("eth_sendUserOperation", [
26
+ userOp,
27
+ this.entryPointAddress,
28
+ ]);
29
+ return userOpHash;
30
+ }
31
+ catch (err) {
32
+ const error = err.error;
33
+ throw new Error(`Failed to send user op with: ${error.message}`);
34
+ }
35
+ }
36
+ async getGasPrice() {
37
+ return this.provider.send("pimlico_getUserOperationGasPrice", []);
38
+ }
39
+ async _getUserOpReceiptInner(userOpHash) {
40
+ return this.provider.send("eth_getUserOperationReceipt", [userOpHash]);
41
+ }
42
+ async getUserOpReceipt(userOpHash) {
43
+ let userOpReceipt = null;
44
+ while (!userOpReceipt) {
45
+ // Wait 2 seconds before checking the status again
46
+ await new Promise((resolve) => setTimeout(resolve, 2000));
47
+ userOpReceipt = await this._getUserOpReceiptInner(userOpHash);
48
+ }
49
+ return userOpReceipt;
50
+ }
51
+ }
52
+ exports.Erc4337Bundler = Erc4337Bundler;
53
+ // TODO(bh2smith) Should probably get reasonable estimates here:
54
+ const defaultPaymasterData = (safeNotDeployed) => {
55
+ return {
56
+ verificationGasLimit: ethers_1.ethers.toBeHex(safeNotDeployed ? 500000 : 100000),
57
+ callGasLimit: ethers_1.ethers.toBeHex(100000),
58
+ preVerificationGas: ethers_1.ethers.toBeHex(100000),
59
+ };
60
+ };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNearSignature = getNearSignature;
4
+ const ethers_1 = require("ethers");
5
+ async function getNearSignature(adapter, hash) {
6
+ const viemHash = typeof hash === "string" ? hash : hash;
7
+ // MPC Contract produces two possible signatures.
8
+ const signature = await adapter.sign(viemHash);
9
+ if (ethers_1.ethers.recoverAddress(hash, signature).toLocaleLowerCase() ===
10
+ adapter.address.toLocaleLowerCase()) {
11
+ return signature;
12
+ }
13
+ throw new Error("Invalid signature!");
14
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContractSuite = void 0;
4
+ const ethers_1 = require("ethers");
5
+ const safe_deployments_1 = require("@safe-global/safe-deployments");
6
+ const safe_modules_deployments_1 = require("@safe-global/safe-modules-deployments");
7
+ const util_1 = require("../util");
8
+ /**
9
+ * All contracts used in account creation & execution
10
+ */
11
+ class ContractSuite {
12
+ constructor(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint) {
13
+ this.provider = provider;
14
+ this.singleton = singleton;
15
+ this.proxyFactory = proxyFactory;
16
+ this.m4337 = m4337;
17
+ this.moduleSetup = moduleSetup;
18
+ this.entryPoint = entryPoint;
19
+ }
20
+ static async init(provider) {
21
+ const safeDeployment = (fn) => getDeployment(fn, { provider, version: "1.4.1" });
22
+ const m4337Deployment = (fn) => getDeployment(fn, { provider, version: "0.3.0" });
23
+ // Need this first to get entryPoint address
24
+ const m4337 = await m4337Deployment(safe_modules_deployments_1.getSafe4337ModuleDeployment);
25
+ const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = await Promise.all([
26
+ safeDeployment(safe_deployments_1.getSafeL2SingletonDeployment),
27
+ safeDeployment(safe_deployments_1.getProxyFactoryDeployment),
28
+ m4337Deployment(safe_modules_deployments_1.getSafeModuleSetupDeployment),
29
+ m4337.SUPPORTED_ENTRYPOINT(),
30
+ ]);
31
+ const entryPoint = new ethers_1.ethers.Contract(supportedEntryPoint, ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], provider);
32
+ return new ContractSuite(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint);
33
+ }
34
+ async addressForSetup(setup, saltNonce) {
35
+ // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
36
+ // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58
37
+ const salt = ethers_1.ethers.keccak256(ethers_1.ethers.solidityPacked(["bytes32", "uint256"], [ethers_1.ethers.keccak256(setup), saltNonce || 0]));
38
+ // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));
39
+ // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29
40
+ const initCode = ethers_1.ethers.solidityPacked(["bytes", "uint256"], [
41
+ await this.proxyFactory.proxyCreationCode(),
42
+ await this.singleton.getAddress(),
43
+ ]);
44
+ return ethers_1.ethers.getCreate2Address(await this.proxyFactory.getAddress(), salt, ethers_1.ethers.keccak256(initCode));
45
+ }
46
+ async getSetup(owners) {
47
+ const setup = await this.singleton.interface.encodeFunctionData("setup", [
48
+ owners,
49
+ 1, // We use sign threshold of 1.
50
+ this.moduleSetup.target,
51
+ this.moduleSetup.interface.encodeFunctionData("enableModules", [
52
+ [this.m4337.target],
53
+ ]),
54
+ this.m4337.target,
55
+ ethers_1.ethers.ZeroAddress,
56
+ 0,
57
+ ethers_1.ethers.ZeroAddress,
58
+ ]);
59
+ return setup;
60
+ }
61
+ async getOpHash(unsignedUserOp, paymasterData) {
62
+ return this.m4337.getOperationHash({
63
+ ...unsignedUserOp,
64
+ initCode: unsignedUserOp.factory
65
+ ? ethers_1.ethers.solidityPacked(["address", "bytes"], [unsignedUserOp.factory, unsignedUserOp.factoryData])
66
+ : "0x",
67
+ accountGasLimits: (0, util_1.packGas)(unsignedUserOp.verificationGasLimit, unsignedUserOp.callGasLimit),
68
+ gasFees: (0, util_1.packGas)(unsignedUserOp.maxPriorityFeePerGas, unsignedUserOp.maxFeePerGas),
69
+ paymasterAndData: (0, util_1.packPaymasterData)(paymasterData),
70
+ signature: util_1.PLACEHOLDER_SIG,
71
+ });
72
+ }
73
+ factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce) {
74
+ return safeNotDeployed
75
+ ? {
76
+ factory: this.proxyFactory.target,
77
+ factoryData: this.proxyFactory.interface.encodeFunctionData("createProxyWithNonce", [this.singleton.target, setup, safeSaltNonce]),
78
+ }
79
+ : {};
80
+ }
81
+ async buildUserOp(txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
82
+ const rawUserOp = {
83
+ sender: safeAddress,
84
+ nonce: ethers_1.ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)),
85
+ ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce),
86
+ // <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
87
+ callData: this.m4337.interface.encodeFunctionData("executeUserOp", [
88
+ txData.to,
89
+ BigInt(txData.value),
90
+ txData.data,
91
+ txData.operation || 0,
92
+ ]),
93
+ ...feeData,
94
+ };
95
+ return rawUserOp;
96
+ }
97
+ }
98
+ exports.ContractSuite = ContractSuite;
99
+ async function getDeployment(fn, { provider, version }) {
100
+ const { chainId } = await provider.getNetwork();
101
+ const deployment = fn({ version });
102
+ if (!deployment || !deployment.networkAddresses[`${chainId}`]) {
103
+ throw new Error(`Deployment not found for version ${version} and chainId ${chainId}`);
104
+ }
105
+ return new ethers_1.ethers.Contract(deployment.networkAddresses[`${chainId}`], deployment.abi, provider);
106
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransactionManager = void 0;
4
+ const ethers_1 = require("ethers");
5
+ const near_ca_1 = require("near-ca");
6
+ const bundler_1 = require("./lib/bundler");
7
+ const util_1 = require("./util");
8
+ const near_1 = require("./lib/near");
9
+ const ethers_multisend_1 = require("ethers-multisend");
10
+ const safe_1 = require("./lib/safe");
11
+ class TransactionManager {
12
+ constructor(provider, nearAdapter, safePack, bundler, setup, safeAddress, safeSaltNonce, safeNotDeployed) {
13
+ this.provider = provider;
14
+ this.nearAdapter = nearAdapter;
15
+ this.safePack = safePack;
16
+ this.bundler = bundler;
17
+ this.setup = setup;
18
+ this.safeAddress = safeAddress;
19
+ this.safeSaltNonce = safeSaltNonce;
20
+ this._safeNotDeployed = safeNotDeployed;
21
+ }
22
+ static async create(config) {
23
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.ethRpc);
24
+ const [nearAdapter, safePack] = await Promise.all([
25
+ near_ca_1.NearEthAdapter.fromConfig({
26
+ mpcContract: new near_ca_1.MpcContract(config.nearAccount, config.mpcContractId),
27
+ }),
28
+ safe_1.ContractSuite.init(provider),
29
+ ]);
30
+ console.log(`Near Adapter: ${nearAdapter.nearAccountId()} <> ${nearAdapter.address}`);
31
+ const bundler = new bundler_1.Erc4337Bundler(config.erc4337BundlerUrl, await safePack.entryPoint.getAddress());
32
+ const setup = await safePack.getSetup([nearAdapter.address]);
33
+ const safeAddress = await safePack.addressForSetup(setup, config.safeSaltNonce);
34
+ const safeNotDeployed = (await provider.getCode(safeAddress)) === "0x";
35
+ console.log(`Safe Address: ${safeAddress} - deployed? ${!safeNotDeployed}`);
36
+ return new TransactionManager(provider, nearAdapter, safePack, bundler, setup, safeAddress, config.safeSaltNonce || "0", safeNotDeployed);
37
+ }
38
+ get safeNotDeployed() {
39
+ return this._safeNotDeployed;
40
+ }
41
+ get nearEOA() {
42
+ return this.nearAdapter.address;
43
+ }
44
+ async getSafeBalance() {
45
+ return await this.provider.getBalance(this.safeAddress);
46
+ }
47
+ async buildTransaction(args) {
48
+ const { transactions, options } = args;
49
+ const gasFees = (await this.bundler.getGasPrice()).fast;
50
+ // const gasFees = await this.provider.getFeeData();
51
+ // Build Singular MetaTransaction for Multisend from transaction list.
52
+ if (transactions.length === 0) {
53
+ throw new Error("Empty transaction set!");
54
+ }
55
+ const tx = transactions.length > 1 ? (0, ethers_multisend_1.encodeMulti)(transactions) : transactions[0];
56
+ const rawUserOp = await this.safePack.buildUserOp(tx, this.safeAddress, gasFees, this.setup, this.safeNotDeployed, this.safeSaltNonce);
57
+ const paymasterData = await this.bundler.getPaymasterData(rawUserOp, options.usePaymaster, this.safeNotDeployed);
58
+ const unsignedUserOp = { ...rawUserOp, ...paymasterData };
59
+ const safeOpHash = await this.safePack.getOpHash(unsignedUserOp, paymasterData);
60
+ return {
61
+ safeOpHash,
62
+ unsignedUserOp,
63
+ };
64
+ }
65
+ async signTransaction(safeOpHash) {
66
+ const signature = await (0, near_1.getNearSignature)(this.nearAdapter, safeOpHash);
67
+ return (0, util_1.packSignature)(signature);
68
+ }
69
+ async executeTransaction(userOp) {
70
+ const userOpHash = await this.bundler.sendUserOperation(userOp);
71
+ console.log("UserOp Hash", userOpHash);
72
+ const userOpReceipt = await this.bundler.getUserOpReceipt(userOpHash);
73
+ console.log("userOp Receipt", userOpReceipt);
74
+ // Update safeNotDeployed after the first transaction
75
+ this._safeNotDeployed =
76
+ (await this.provider.getCode(this.safeAddress)) === "0x";
77
+ return userOpReceipt;
78
+ }
79
+ addOwnerTx(address) {
80
+ return {
81
+ to: this.safeAddress,
82
+ value: "0",
83
+ data: this.safePack.singleton.interface.encodeFunctionData("addOwnerWithThreshold", [address, 1]),
84
+ };
85
+ }
86
+ async safeSufficientlyFunded(transactions, gasCost) {
87
+ const txValue = transactions.reduce((acc, tx) => acc + BigInt(tx.value), 0n);
88
+ if (txValue + gasCost === 0n) {
89
+ return true;
90
+ }
91
+ const safeBalance = await this.getSafeBalance();
92
+ return txValue + gasCost < safeBalance;
93
+ }
94
+ }
95
+ exports.TransactionManager = TransactionManager;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packGas = exports.PLACEHOLDER_SIG = void 0;
4
+ exports.packSignature = packSignature;
5
+ exports.packPaymasterData = packPaymasterData;
6
+ exports.containsValue = containsValue;
7
+ const ethers_1 = require("ethers");
8
+ exports.PLACEHOLDER_SIG = ethers_1.ethers.solidityPacked(["uint48", "uint48"], [0, 0]);
9
+ const packGas = (hi, lo) => ethers_1.ethers.solidityPacked(["uint128", "uint128"], [hi, lo]);
10
+ exports.packGas = packGas;
11
+ function packSignature(signature, validFrom = 0, validTo = 0) {
12
+ return ethers_1.ethers.solidityPacked(["uint48", "uint48", "bytes"], [validFrom, validTo, signature]);
13
+ }
14
+ function packPaymasterData(data) {
15
+ return data.paymaster
16
+ ? ethers_1.ethers.hexlify(ethers_1.ethers.concat([
17
+ data.paymaster,
18
+ ethers_1.ethers.toBeHex(data.paymasterVerificationGasLimit || "0x", 16),
19
+ ethers_1.ethers.toBeHex(data.paymasterPostOpGasLimit || "0x", 16),
20
+ data.paymasterData || "0x",
21
+ ]))
22
+ : "0x";
23
+ }
24
+ function containsValue(transactions) {
25
+ return transactions.some((tx) => tx.value !== "0");
26
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./tx-manager";
2
+ export * from "./types";
3
+ export * from "./util";
@@ -0,0 +1,3 @@
1
+ export * from "./tx-manager";
2
+ export * from "./types";
3
+ export * from "./util";
@@ -0,0 +1,12 @@
1
+ import { ethers } from "ethers";
2
+ import { GasPrices, PaymasterData, UnsignedUserOperation, UserOperation, UserOperationReceipt } from "../types";
3
+ export declare class Erc4337Bundler {
4
+ provider: ethers.JsonRpcProvider;
5
+ entryPointAddress: string;
6
+ constructor(bundlerUrl: string, entryPointAddress: string);
7
+ getPaymasterData(rawUserOp: UnsignedUserOperation, usePaymaster: boolean, safeNotDeployed: boolean): Promise<PaymasterData>;
8
+ sendUserOperation(userOp: UserOperation): Promise<string>;
9
+ getGasPrice(): Promise<GasPrices>;
10
+ _getUserOpReceiptInner(userOpHash: string): Promise<UserOperationReceipt | null>;
11
+ getUserOpReceipt(userOpHash: string): Promise<UserOperationReceipt>;
12
+ }
@@ -0,0 +1,58 @@
1
+ import { ethers } from "ethers";
2
+ import { PLACEHOLDER_SIG } from "../util";
3
+ export class Erc4337Bundler {
4
+ provider;
5
+ entryPointAddress;
6
+ constructor(bundlerUrl, entryPointAddress) {
7
+ this.entryPointAddress = entryPointAddress;
8
+ this.provider = new ethers.JsonRpcProvider(bundlerUrl);
9
+ }
10
+ async getPaymasterData(rawUserOp, usePaymaster, safeNotDeployed) {
11
+ // TODO: Keep this option out of the bundler
12
+ if (usePaymaster) {
13
+ console.log("Requesting paymaster data...");
14
+ const data = this.provider.send("pm_sponsorUserOperation", [
15
+ { ...rawUserOp, signature: PLACEHOLDER_SIG },
16
+ this.entryPointAddress,
17
+ ]);
18
+ return data;
19
+ }
20
+ return defaultPaymasterData(safeNotDeployed);
21
+ }
22
+ async sendUserOperation(userOp) {
23
+ try {
24
+ const userOpHash = await this.provider.send("eth_sendUserOperation", [
25
+ userOp,
26
+ this.entryPointAddress,
27
+ ]);
28
+ return userOpHash;
29
+ }
30
+ catch (err) {
31
+ const error = err.error;
32
+ throw new Error(`Failed to send user op with: ${error.message}`);
33
+ }
34
+ }
35
+ async getGasPrice() {
36
+ return this.provider.send("pimlico_getUserOperationGasPrice", []);
37
+ }
38
+ async _getUserOpReceiptInner(userOpHash) {
39
+ return this.provider.send("eth_getUserOperationReceipt", [userOpHash]);
40
+ }
41
+ async getUserOpReceipt(userOpHash) {
42
+ let userOpReceipt = null;
43
+ while (!userOpReceipt) {
44
+ // Wait 2 seconds before checking the status again
45
+ await new Promise((resolve) => setTimeout(resolve, 2000));
46
+ userOpReceipt = await this._getUserOpReceiptInner(userOpHash);
47
+ }
48
+ return userOpReceipt;
49
+ }
50
+ }
51
+ // TODO(bh2smith) Should probably get reasonable estimates here:
52
+ const defaultPaymasterData = (safeNotDeployed) => {
53
+ return {
54
+ verificationGasLimit: ethers.toBeHex(safeNotDeployed ? 500000 : 100000),
55
+ callGasLimit: ethers.toBeHex(100000),
56
+ preVerificationGas: ethers.toBeHex(100000),
57
+ };
58
+ };
@@ -0,0 +1,3 @@
1
+ import { ethers } from "ethers";
2
+ import { NearEthAdapter } from "near-ca";
3
+ export declare function getNearSignature(adapter: NearEthAdapter, hash: ethers.BytesLike): Promise<string>;
@@ -0,0 +1,11 @@
1
+ import { ethers } from "ethers";
2
+ export async function getNearSignature(adapter, hash) {
3
+ const viemHash = typeof hash === "string" ? hash : hash;
4
+ // MPC Contract produces two possible signatures.
5
+ const signature = await adapter.sign(viemHash);
6
+ if (ethers.recoverAddress(hash, signature).toLocaleLowerCase() ===
7
+ adapter.address.toLocaleLowerCase()) {
8
+ return signature;
9
+ }
10
+ throw new Error("Invalid signature!");
11
+ }
@@ -0,0 +1,24 @@
1
+ import { ethers } from "ethers";
2
+ import { GasPrice, PaymasterData, UnsignedUserOperation, UserOperation } from "../types";
3
+ import { MetaTransaction } from "ethers-multisend";
4
+ /**
5
+ * All contracts used in account creation & execution
6
+ */
7
+ 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);
15
+ static init(provider: ethers.JsonRpcProvider): Promise<ContractSuite>;
16
+ addressForSetup(setup: ethers.BytesLike, saltNonce?: string): Promise<string>;
17
+ getSetup(owners: string[]): Promise<string>;
18
+ getOpHash(unsignedUserOp: UserOperation, paymasterData: PaymasterData): Promise<string>;
19
+ factoryDataForSetup(safeNotDeployed: boolean, setup: string, safeSaltNonce: string): {
20
+ factory?: ethers.AddressLike;
21
+ factoryData?: string;
22
+ };
23
+ buildUserOp(txData: MetaTransaction, safeAddress: ethers.AddressLike, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
24
+ }
@@ -0,0 +1,108 @@
1
+ import { ethers } from "ethers";
2
+ import { getProxyFactoryDeployment, getSafeL2SingletonDeployment, } from "@safe-global/safe-deployments";
3
+ import { getSafe4337ModuleDeployment, getSafeModuleSetupDeployment, } from "@safe-global/safe-modules-deployments";
4
+ import { PLACEHOLDER_SIG, packGas, packPaymasterData } from "../util";
5
+ /**
6
+ * All contracts used in account creation & execution
7
+ */
8
+ export class ContractSuite {
9
+ provider;
10
+ singleton;
11
+ proxyFactory;
12
+ m4337;
13
+ moduleSetup;
14
+ entryPoint;
15
+ constructor(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint) {
16
+ this.provider = provider;
17
+ this.singleton = singleton;
18
+ this.proxyFactory = proxyFactory;
19
+ this.m4337 = m4337;
20
+ this.moduleSetup = moduleSetup;
21
+ this.entryPoint = entryPoint;
22
+ }
23
+ static async init(provider) {
24
+ const safeDeployment = (fn) => getDeployment(fn, { provider, version: "1.4.1" });
25
+ const m4337Deployment = (fn) => getDeployment(fn, { provider, version: "0.3.0" });
26
+ // Need this first to get entryPoint address
27
+ const m4337 = await m4337Deployment(getSafe4337ModuleDeployment);
28
+ const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = await Promise.all([
29
+ safeDeployment(getSafeL2SingletonDeployment),
30
+ safeDeployment(getProxyFactoryDeployment),
31
+ m4337Deployment(getSafeModuleSetupDeployment),
32
+ m4337.SUPPORTED_ENTRYPOINT(),
33
+ ]);
34
+ const entryPoint = new ethers.Contract(supportedEntryPoint, ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], provider);
35
+ return new ContractSuite(provider, singleton, proxyFactory, m4337, moduleSetup, entryPoint);
36
+ }
37
+ async addressForSetup(setup, saltNonce) {
38
+ // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
39
+ // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58
40
+ const salt = ethers.keccak256(ethers.solidityPacked(["bytes32", "uint256"], [ethers.keccak256(setup), saltNonce || 0]));
41
+ // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));
42
+ // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29
43
+ const initCode = ethers.solidityPacked(["bytes", "uint256"], [
44
+ await this.proxyFactory.proxyCreationCode(),
45
+ await this.singleton.getAddress(),
46
+ ]);
47
+ return ethers.getCreate2Address(await this.proxyFactory.getAddress(), salt, ethers.keccak256(initCode));
48
+ }
49
+ async getSetup(owners) {
50
+ const setup = await this.singleton.interface.encodeFunctionData("setup", [
51
+ owners,
52
+ 1, // We use sign threshold of 1.
53
+ this.moduleSetup.target,
54
+ this.moduleSetup.interface.encodeFunctionData("enableModules", [
55
+ [this.m4337.target],
56
+ ]),
57
+ this.m4337.target,
58
+ ethers.ZeroAddress,
59
+ 0,
60
+ ethers.ZeroAddress,
61
+ ]);
62
+ return setup;
63
+ }
64
+ async getOpHash(unsignedUserOp, paymasterData) {
65
+ return this.m4337.getOperationHash({
66
+ ...unsignedUserOp,
67
+ initCode: unsignedUserOp.factory
68
+ ? ethers.solidityPacked(["address", "bytes"], [unsignedUserOp.factory, unsignedUserOp.factoryData])
69
+ : "0x",
70
+ accountGasLimits: packGas(unsignedUserOp.verificationGasLimit, unsignedUserOp.callGasLimit),
71
+ gasFees: packGas(unsignedUserOp.maxPriorityFeePerGas, unsignedUserOp.maxFeePerGas),
72
+ paymasterAndData: packPaymasterData(paymasterData),
73
+ signature: PLACEHOLDER_SIG,
74
+ });
75
+ }
76
+ factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce) {
77
+ return safeNotDeployed
78
+ ? {
79
+ factory: this.proxyFactory.target,
80
+ factoryData: this.proxyFactory.interface.encodeFunctionData("createProxyWithNonce", [this.singleton.target, setup, safeSaltNonce]),
81
+ }
82
+ : {};
83
+ }
84
+ async buildUserOp(txData, safeAddress, feeData, setup, safeNotDeployed, safeSaltNonce) {
85
+ const rawUserOp = {
86
+ sender: safeAddress,
87
+ nonce: ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)),
88
+ ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce),
89
+ // <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
90
+ callData: this.m4337.interface.encodeFunctionData("executeUserOp", [
91
+ txData.to,
92
+ BigInt(txData.value),
93
+ txData.data,
94
+ txData.operation || 0,
95
+ ]),
96
+ ...feeData,
97
+ };
98
+ return rawUserOp;
99
+ }
100
+ }
101
+ async function getDeployment(fn, { provider, version }) {
102
+ const { chainId } = await provider.getNetwork();
103
+ const deployment = fn({ version });
104
+ if (!deployment || !deployment.networkAddresses[`${chainId}`]) {
105
+ throw new Error(`Deployment not found for version ${version} and chainId ${chainId}`);
106
+ }
107
+ return new ethers.Contract(deployment.networkAddresses[`${chainId}`], deployment.abi, provider);
108
+ }
@@ -0,0 +1,39 @@
1
+ import { ethers } from "ethers";
2
+ import { NearEthAdapter } from "near-ca";
3
+ import { Erc4337Bundler } from "./lib/bundler";
4
+ import { UserOperation, UserOperationReceipt, UserOptions } from "./types";
5
+ import { MetaTransaction } from "ethers-multisend";
6
+ import { ContractSuite } from "./lib/safe";
7
+ import { Account } from "near-api-js";
8
+ export declare class TransactionManager {
9
+ readonly provider: ethers.JsonRpcProvider;
10
+ readonly nearAdapter: NearEthAdapter;
11
+ private safePack;
12
+ private bundler;
13
+ private setup;
14
+ readonly safeAddress: string;
15
+ private safeSaltNonce;
16
+ private _safeNotDeployed;
17
+ constructor(provider: ethers.JsonRpcProvider, nearAdapter: NearEthAdapter, safePack: ContractSuite, bundler: Erc4337Bundler, setup: string, safeAddress: string, safeSaltNonce: string, safeNotDeployed: boolean);
18
+ static create(config: {
19
+ ethRpc: string;
20
+ erc4337BundlerUrl: string;
21
+ nearAccount: Account;
22
+ mpcContractId: string;
23
+ safeSaltNonce?: string;
24
+ }): Promise<TransactionManager>;
25
+ get safeNotDeployed(): boolean;
26
+ get nearEOA(): `0x${string}`;
27
+ getSafeBalance(): Promise<bigint>;
28
+ buildTransaction(args: {
29
+ transactions: MetaTransaction[];
30
+ options: UserOptions;
31
+ }): Promise<{
32
+ safeOpHash: string;
33
+ unsignedUserOp: UserOperation;
34
+ }>;
35
+ signTransaction(safeOpHash: string): Promise<string>;
36
+ executeTransaction(userOp: UserOperation): Promise<UserOperationReceipt>;
37
+ addOwnerTx(address: string): MetaTransaction;
38
+ safeSufficientlyFunded(transactions: MetaTransaction[], gasCost: bigint): Promise<boolean>;
39
+ }
@@ -0,0 +1,99 @@
1
+ import { ethers } from "ethers";
2
+ import { NearEthAdapter, MpcContract } from "near-ca";
3
+ import { Erc4337Bundler } from "./lib/bundler";
4
+ import { packSignature } from "./util";
5
+ import { getNearSignature } from "./lib/near";
6
+ import { encodeMulti } from "ethers-multisend";
7
+ import { ContractSuite } from "./lib/safe";
8
+ export class TransactionManager {
9
+ provider;
10
+ nearAdapter;
11
+ safePack;
12
+ bundler;
13
+ setup;
14
+ safeAddress;
15
+ safeSaltNonce;
16
+ _safeNotDeployed;
17
+ constructor(provider, nearAdapter, safePack, bundler, setup, safeAddress, safeSaltNonce, safeNotDeployed) {
18
+ this.provider = provider;
19
+ this.nearAdapter = nearAdapter;
20
+ this.safePack = safePack;
21
+ this.bundler = bundler;
22
+ this.setup = setup;
23
+ this.safeAddress = safeAddress;
24
+ this.safeSaltNonce = safeSaltNonce;
25
+ this._safeNotDeployed = safeNotDeployed;
26
+ }
27
+ static async create(config) {
28
+ const provider = new ethers.JsonRpcProvider(config.ethRpc);
29
+ const [nearAdapter, safePack] = await Promise.all([
30
+ NearEthAdapter.fromConfig({
31
+ mpcContract: new MpcContract(config.nearAccount, config.mpcContractId),
32
+ }),
33
+ ContractSuite.init(provider),
34
+ ]);
35
+ console.log(`Near Adapter: ${nearAdapter.nearAccountId()} <> ${nearAdapter.address}`);
36
+ const bundler = new Erc4337Bundler(config.erc4337BundlerUrl, await safePack.entryPoint.getAddress());
37
+ const setup = await safePack.getSetup([nearAdapter.address]);
38
+ const safeAddress = await safePack.addressForSetup(setup, config.safeSaltNonce);
39
+ const safeNotDeployed = (await provider.getCode(safeAddress)) === "0x";
40
+ console.log(`Safe Address: ${safeAddress} - deployed? ${!safeNotDeployed}`);
41
+ return new TransactionManager(provider, nearAdapter, safePack, bundler, setup, safeAddress, config.safeSaltNonce || "0", safeNotDeployed);
42
+ }
43
+ get safeNotDeployed() {
44
+ return this._safeNotDeployed;
45
+ }
46
+ get nearEOA() {
47
+ return this.nearAdapter.address;
48
+ }
49
+ async getSafeBalance() {
50
+ return await this.provider.getBalance(this.safeAddress);
51
+ }
52
+ async buildTransaction(args) {
53
+ const { transactions, options } = args;
54
+ const gasFees = (await this.bundler.getGasPrice()).fast;
55
+ // const gasFees = await this.provider.getFeeData();
56
+ // Build Singular MetaTransaction for Multisend from transaction list.
57
+ if (transactions.length === 0) {
58
+ throw new Error("Empty transaction set!");
59
+ }
60
+ const tx = transactions.length > 1 ? encodeMulti(transactions) : transactions[0];
61
+ const rawUserOp = await this.safePack.buildUserOp(tx, this.safeAddress, gasFees, this.setup, this.safeNotDeployed, this.safeSaltNonce);
62
+ const paymasterData = await this.bundler.getPaymasterData(rawUserOp, options.usePaymaster, this.safeNotDeployed);
63
+ const unsignedUserOp = { ...rawUserOp, ...paymasterData };
64
+ const safeOpHash = await this.safePack.getOpHash(unsignedUserOp, paymasterData);
65
+ return {
66
+ safeOpHash,
67
+ unsignedUserOp,
68
+ };
69
+ }
70
+ async signTransaction(safeOpHash) {
71
+ const signature = await getNearSignature(this.nearAdapter, safeOpHash);
72
+ return packSignature(signature);
73
+ }
74
+ async executeTransaction(userOp) {
75
+ const userOpHash = await this.bundler.sendUserOperation(userOp);
76
+ console.log("UserOp Hash", userOpHash);
77
+ const userOpReceipt = await this.bundler.getUserOpReceipt(userOpHash);
78
+ console.log("userOp Receipt", userOpReceipt);
79
+ // Update safeNotDeployed after the first transaction
80
+ this._safeNotDeployed =
81
+ (await this.provider.getCode(this.safeAddress)) === "0x";
82
+ return userOpReceipt;
83
+ }
84
+ addOwnerTx(address) {
85
+ return {
86
+ to: this.safeAddress,
87
+ value: "0",
88
+ data: this.safePack.singleton.interface.encodeFunctionData("addOwnerWithThreshold", [address, 1]),
89
+ };
90
+ }
91
+ async safeSufficientlyFunded(transactions, gasCost) {
92
+ const txValue = transactions.reduce((acc, tx) => acc + BigInt(tx.value), 0n);
93
+ if (txValue + gasCost === 0n) {
94
+ return true;
95
+ }
96
+ const safeBalance = await this.getSafeBalance();
97
+ return txValue + gasCost < safeBalance;
98
+ }
99
+ }
@@ -0,0 +1,85 @@
1
+ import { ethers } from "ethers";
2
+ export interface UnsignedUserOperation {
3
+ sender: ethers.AddressLike;
4
+ nonce: string;
5
+ factory?: ethers.AddressLike;
6
+ factoryData?: ethers.BytesLike;
7
+ callData: string;
8
+ maxPriorityFeePerGas: string;
9
+ maxFeePerGas: string;
10
+ }
11
+ /**
12
+ * Supported Representation of UserOperation for EntryPoint v0.7
13
+ */
14
+ export interface UserOperation extends UnsignedUserOperation {
15
+ verificationGasLimit: string;
16
+ callGasLimit: string;
17
+ preVerificationGas: string;
18
+ signature?: string;
19
+ }
20
+ export interface PaymasterData {
21
+ paymaster?: string;
22
+ paymasterData?: string;
23
+ paymasterVerificationGasLimit?: string;
24
+ paymasterPostOpGasLimit?: string;
25
+ verificationGasLimit: string;
26
+ callGasLimit: string;
27
+ preVerificationGas: string;
28
+ }
29
+ export interface UserOptions {
30
+ usePaymaster: boolean;
31
+ safeSaltNonce: string;
32
+ mpcContractId: string;
33
+ recoveryAddress?: string;
34
+ }
35
+ export type TStatus = "success" | "reverted";
36
+ export type Address = ethers.AddressLike;
37
+ export type Hex = `0x${string}`;
38
+ export type Hash = `0x${string}`;
39
+ interface Log {
40
+ logIndex: string;
41
+ transactionIndex: string;
42
+ transactionHash: string;
43
+ blockHash: string;
44
+ blockNumber: string;
45
+ address: string;
46
+ data: string;
47
+ topics: string[];
48
+ }
49
+ interface Receipt {
50
+ transactionHash: Hex;
51
+ transactionIndex: bigint;
52
+ blockHash: Hash;
53
+ blockNumber: bigint;
54
+ from: Address;
55
+ to: Address | null;
56
+ cumulativeGasUsed: bigint;
57
+ status: TStatus;
58
+ gasUsed: bigint;
59
+ contractAddress: Address | null;
60
+ logsBloom: Hex;
61
+ effectiveGasPrice: bigint;
62
+ }
63
+ export type UserOperationReceipt = {
64
+ userOpHash: Hash;
65
+ entryPoint: Address;
66
+ sender: Address;
67
+ nonce: bigint;
68
+ paymaster?: Address;
69
+ actualGasUsed: bigint;
70
+ actualGasCost: bigint;
71
+ success: boolean;
72
+ reason?: string;
73
+ receipt: Receipt;
74
+ logs: Log[];
75
+ };
76
+ export interface GasPrices {
77
+ slow: GasPrice;
78
+ standard: GasPrice;
79
+ fast: GasPrice;
80
+ }
81
+ export interface GasPrice {
82
+ maxFeePerGas: Hex;
83
+ maxPriorityFeePerGas: Hex;
84
+ }
85
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { ethers } from "ethers";
2
+ import { PaymasterData } from "./types";
3
+ import { MetaTransaction } from "ethers-multisend";
4
+ export declare const PLACEHOLDER_SIG: string;
5
+ export declare const packGas: (hi: ethers.BigNumberish, lo: ethers.BigNumberish) => string;
6
+ export declare function packSignature(signature: string, validFrom?: number, validTo?: number): string;
7
+ export declare function packPaymasterData(data: PaymasterData): string;
8
+ export declare function containsValue(transactions: MetaTransaction[]): boolean;
@@ -0,0 +1,19 @@
1
+ import { ethers } from "ethers";
2
+ export const PLACEHOLDER_SIG = ethers.solidityPacked(["uint48", "uint48"], [0, 0]);
3
+ export const packGas = (hi, lo) => ethers.solidityPacked(["uint128", "uint128"], [hi, lo]);
4
+ export function packSignature(signature, validFrom = 0, validTo = 0) {
5
+ return ethers.solidityPacked(["uint48", "uint48", "bytes"], [validFrom, validTo, signature]);
6
+ }
7
+ export function packPaymasterData(data) {
8
+ return data.paymaster
9
+ ? ethers.hexlify(ethers.concat([
10
+ data.paymaster,
11
+ ethers.toBeHex(data.paymasterVerificationGasLimit || "0x", 16),
12
+ ethers.toBeHex(data.paymasterPostOpGasLimit || "0x", 16),
13
+ data.paymasterData || "0x",
14
+ ]))
15
+ : "0x";
16
+ }
17
+ export function containsValue(transactions) {
18
+ return transactions.some((tx) => tx.value !== "0");
19
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "near-safe",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "main": "dist/cjs/index.js",
6
+ "module": "dist/cjs/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "files": [
9
+ "dist/**/*"
10
+ ],
11
+ "scripts": {
12
+ "build": "rm -fr dist/* && yarn build:esm && yarn build:cjs",
13
+ "build:esm": "tsc -p tsconfig.esm.json",
14
+ "build:cjs": "tsc -p tsconfig.cjs.json",
15
+ "start": "yarn example",
16
+ "example": "tsx examples/send-tx.ts",
17
+ "lint": "eslint . --ignore-pattern dist/",
18
+ "fmt": "prettier --write '{src,examples,tests}/**/*.{js,jsx,ts,tsx}'",
19
+ "all": "yarn fmt && yarn lint && yarn build"
20
+ },
21
+ "dependencies": {
22
+ "@safe-global/safe-deployments": "^1.37.0",
23
+ "@safe-global/safe-modules-deployments": "^2.2.0",
24
+ "ethers": "^6.13.1",
25
+ "ethers-multisend": "^3.1.0",
26
+ "near-api-js": "^4.0.3",
27
+ "near-ca": "^0.3.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.3.0",
31
+ "@types/yargs": "^17.0.32",
32
+ "@typescript-eslint/eslint-plugin": "^8.1.0",
33
+ "@typescript-eslint/parser": "^8.1.0",
34
+ "dotenv": "^16.4.5",
35
+ "eslint": "^9.6.0",
36
+ "prettier": "^3.3.2",
37
+ "tsx": "^4.16.0",
38
+ "typescript": "^5.5.2",
39
+ "yargs": "^17.7.2"
40
+ }
41
+ }