near-safe 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }