near-safe 0.7.6 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,2 +1,3 @@
1
1
  export declare const USER_OP_IDENTIFIER: `0x${string}`;
2
2
  export declare const DEFAULT_SAFE_SALT_NONCE: string;
3
+ export declare const SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001";
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_SAFE_SALT_NONCE = exports.USER_OP_IDENTIFIER = void 0;
3
+ exports.SENTINEL_OWNERS = exports.DEFAULT_SAFE_SALT_NONCE = exports.USER_OP_IDENTIFIER = void 0;
4
4
  const viem_1 = require("viem");
5
5
  const DOMAIN_SEPARATOR = "bitte/near-safe";
6
6
  // 0x62697474652f6e6561722d7361666500
7
7
  exports.USER_OP_IDENTIFIER = (0, viem_1.toHex)(DOMAIN_SEPARATOR, { size: 16 });
8
8
  // 130811896738364114529934864114944206080
9
9
  exports.DEFAULT_SAFE_SALT_NONCE = BigInt(exports.USER_OP_IDENTIFIER).toString();
10
+ exports.SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001";
@@ -0,0 +1,14 @@
1
+ import { EIP712TypedData } from "near-ca";
2
+ import { Hex, TransactionSerializable } from "viem";
3
+ import { DecodedTxData, SafeEncodedSignRequest, UserOperation } from "./types";
4
+ /**
5
+ * Decodes transaction data for a given EVM transaction and extracts relevant details.
6
+ *
7
+ * @param {EvmTransactionData} data - The raw transaction data to be decoded.
8
+ * @returns {DecodedTxData} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
9
+ */
10
+ export declare function decodeTxData({ evmMessage, chainId, }: SafeEncodedSignRequest): DecodedTxData;
11
+ export declare function decodeTransactionSerializable(chainId: number, tx: TransactionSerializable): DecodedTxData;
12
+ export declare function decodeRlpHex(chainId: number, tx: Hex): DecodedTxData;
13
+ export declare function decodeTypedData(chainId: number, data: EIP712TypedData): DecodedTxData;
14
+ export declare function decodeUserOperation(chainId: number, userOp: UserOperation): DecodedTxData;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decodeTxData = decodeTxData;
4
+ exports.decodeTransactionSerializable = decodeTransactionSerializable;
5
+ exports.decodeRlpHex = decodeRlpHex;
6
+ exports.decodeTypedData = decodeTypedData;
7
+ exports.decodeUserOperation = decodeUserOperation;
8
+ const ethers_multisend_1 = require("ethers-multisend");
9
+ const viem_1 = require("viem");
10
+ const deployments_1 = require("./_gen/deployments");
11
+ const multisend_1 = require("./lib/multisend");
12
+ const safe_message_1 = require("./lib/safe-message");
13
+ /**
14
+ * Decodes transaction data for a given EVM transaction and extracts relevant details.
15
+ *
16
+ * @param {EvmTransactionData} data - The raw transaction data to be decoded.
17
+ * @returns {DecodedTxData} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
18
+ */
19
+ function decodeTxData({ evmMessage, chainId, }) {
20
+ const data = evmMessage;
21
+ if ((0, safe_message_1.isRlpHex)(evmMessage)) {
22
+ decodeRlpHex(chainId, evmMessage);
23
+ }
24
+ if ((0, safe_message_1.isTransactionSerializable)(data)) {
25
+ return decodeTransactionSerializable(chainId, data);
26
+ }
27
+ if (typeof data !== "string") {
28
+ return decodeTypedData(chainId, data);
29
+ }
30
+ try {
31
+ // Stringified UserOperation.
32
+ const userOp = JSON.parse(data);
33
+ return decodeUserOperation(chainId, userOp);
34
+ }
35
+ catch (error) {
36
+ if (error instanceof SyntaxError) {
37
+ // Raw message string.
38
+ return {
39
+ chainId,
40
+ costEstimate: "0",
41
+ transactions: [],
42
+ message: data,
43
+ };
44
+ }
45
+ else {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ throw new Error(`decodeTxData: Unexpected error - ${message}`);
48
+ }
49
+ }
50
+ }
51
+ function decodeTransactionSerializable(chainId, tx) {
52
+ const { gas, maxFeePerGas, maxPriorityFeePerGas, to } = tx;
53
+ if (chainId !== tx.chainId) {
54
+ throw Error(`Transaction chainId mismatch ${chainId} != ${tx.chainId}`);
55
+ }
56
+ if (!gas || !maxFeePerGas || !maxPriorityFeePerGas) {
57
+ throw Error(`Insufficient feeData for ${(0, viem_1.serializeTransaction)(tx)}. Check https://rawtxdecode.in/`);
58
+ }
59
+ if (!to) {
60
+ throw Error(`Transaction is missing the 'to' in ${(0, viem_1.serializeTransaction)(tx)}. Check https://rawtxdecode.in/`);
61
+ }
62
+ return {
63
+ chainId,
64
+ // This is an upper bound on the gas fees (could be lower)
65
+ costEstimate: (0, viem_1.formatEther)(gas * (maxFeePerGas + maxPriorityFeePerGas)),
66
+ transactions: [
67
+ {
68
+ to,
69
+ value: (tx.value || 0n).toString(),
70
+ data: tx.data || "0x",
71
+ },
72
+ ],
73
+ };
74
+ }
75
+ function decodeRlpHex(chainId, tx) {
76
+ return decodeTransactionSerializable(chainId, (0, viem_1.parseTransaction)(tx));
77
+ }
78
+ function decodeTypedData(chainId, data) {
79
+ return {
80
+ chainId,
81
+ costEstimate: "0",
82
+ transactions: [],
83
+ message: data,
84
+ };
85
+ }
86
+ function decodeUserOperation(chainId, userOp) {
87
+ const { callGasLimit, maxFeePerGas, maxPriorityFeePerGas } = userOp;
88
+ const maxGasPrice = BigInt(maxFeePerGas) + BigInt(maxPriorityFeePerGas);
89
+ const { args } = (0, viem_1.decodeFunctionData)({
90
+ abi: deployments_1.SAFE_DEPLOYMENTS.m4337.abi,
91
+ data: userOp.callData,
92
+ });
93
+ // Determine if singular or double!
94
+ const transactions = (0, multisend_1.isMultisendTx)(args)
95
+ ? (0, ethers_multisend_1.decodeMulti)(args[2])
96
+ : [
97
+ {
98
+ to: args[0],
99
+ value: args[1],
100
+ data: args[2],
101
+ operation: args[3],
102
+ },
103
+ ];
104
+ return {
105
+ chainId,
106
+ // This is an upper bound on the gas fees (could be lower)
107
+ costEstimate: (0, viem_1.formatEther)(BigInt(callGasLimit) * maxGasPrice),
108
+ transactions,
109
+ };
110
+ }
@@ -1,6 +1,6 @@
1
1
  import { type SafeInfo } from "@safe-global/safe-gateway-typescript-sdk";
2
2
  import { EIP712TypedData } from "near-ca";
3
- import { Hash } from "viem";
3
+ import { Hash, Hex, TransactionSerializable } from "viem";
4
4
  export type DecodedSafeMessage = {
5
5
  decodedMessage: string | EIP712TypedData;
6
6
  safeMessageMessage: string;
@@ -20,3 +20,5 @@ export type MinimalSafeInfo = Pick<SafeInfo, "address" | "version" | "chainId">;
20
20
  * }`
21
21
  */
22
22
  export declare function decodeSafeMessage(message: string | EIP712TypedData, safe: MinimalSafeInfo): DecodedSafeMessage;
23
+ export declare function isTransactionSerializable(data: unknown): data is TransactionSerializable;
24
+ export declare function isRlpHex(data: unknown): data is Hex;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.decodeSafeMessage = decodeSafeMessage;
4
+ exports.isTransactionSerializable = isTransactionSerializable;
5
+ exports.isRlpHex = isRlpHex;
4
6
  const semver_1 = require("semver");
5
7
  const viem_1 = require("viem");
6
8
  /*
@@ -93,3 +95,22 @@ function decodeSafeMessage(message, safe) {
93
95
  // };
94
96
  // export const isBlindSigningPayload = (obj: EIP712TypedData | string): boolean =>
95
97
  // !isEIP712TypedData(obj) && isHash(obj);
98
+ // Cheeky attempt to serialize. return true if successful!
99
+ function isTransactionSerializable(data) {
100
+ try {
101
+ (0, viem_1.serializeTransaction)(data);
102
+ return true;
103
+ }
104
+ catch (error) {
105
+ return false;
106
+ }
107
+ }
108
+ function isRlpHex(data) {
109
+ try {
110
+ (0, viem_1.parseTransaction)(data);
111
+ return true;
112
+ }
113
+ catch (error) {
114
+ return false;
115
+ }
116
+ }
@@ -14,8 +14,10 @@ export declare class SafeContractSuite {
14
14
  addressForSetup(setup: Hex, saltNonce: string): Promise<Address>;
15
15
  getSetup(owners: string[]): Hex;
16
16
  addOwnerData(newOwner: Address): Hex;
17
+ removeOwnerData(chainId: number, safeAddress: Address, owner: Address): Promise<Hex>;
17
18
  getOpHash(chainId: number, unsignedUserOp: UserOperation): Promise<Hash>;
18
19
  private factoryDataForSetup;
19
20
  buildUserOp(nonce: bigint, txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
20
21
  getNonce(address: Address, chainId: number): Promise<bigint>;
22
+ prevOwner(chainId: number, safeAddress: Address, owner: Address): Promise<Address>;
21
23
  }
@@ -65,6 +65,15 @@ class SafeContractSuite {
65
65
  args: [newOwner, 1],
66
66
  });
67
67
  }
68
+ async removeOwnerData(chainId, safeAddress, owner) {
69
+ const prevOwner = await this.prevOwner(chainId, safeAddress, owner);
70
+ return (0, viem_1.encodeFunctionData)({
71
+ abi: this.singleton.abi,
72
+ functionName: "removeOwner",
73
+ // Keep threshold at 1!
74
+ args: [prevOwner, owner, 1],
75
+ });
76
+ }
68
77
  async getOpHash(chainId, unsignedUserOp) {
69
78
  const { factory, factoryData, verificationGasLimit, callGasLimit, maxPriorityFeePerGas, maxFeePerGas, } = unsignedUserOp;
70
79
  const client = await (0, util_1.getClient)(chainId);
@@ -131,5 +140,21 @@ class SafeContractSuite {
131
140
  }));
132
141
  return nonce;
133
142
  }
143
+ async prevOwner(chainId, safeAddress, owner) {
144
+ const client = (0, util_1.getClient)(chainId);
145
+ const currentOwners = await client.readContract({
146
+ address: safeAddress,
147
+ // abi: this.singleton.abi,
148
+ abi: (0, viem_1.parseAbi)([
149
+ "function getOwners() public view returns (address[] memory)",
150
+ ]),
151
+ functionName: "getOwners",
152
+ });
153
+ const ownerIndex = currentOwners.findIndex((t) => t === owner);
154
+ if (ownerIndex === -1) {
155
+ throw new Error(`Not a current owner: ${owner}`);
156
+ }
157
+ return ownerIndex > 0 ? currentOwners[ownerIndex - 1] : constants_1.SENTINEL_OWNERS;
158
+ }
134
159
  }
135
160
  exports.SafeContractSuite = SafeContractSuite;
@@ -1,9 +1,9 @@
1
1
  import { NearConfig } from "near-api-js/lib/near";
2
2
  import { FinalExecutionOutcome } from "near-api-js/lib/providers";
3
- import { NearEthAdapter, SignRequestData } from "near-ca";
3
+ import { NearEthAdapter, SignRequestData, EncodedSignRequest } from "near-ca";
4
4
  import { Address, Hash, Hex } from "viem";
5
5
  import { SafeContractSuite } from "./lib/safe";
6
- import { DecodedMultisend, EncodedTxData, EvmTransactionData, MetaTransaction, SponsorshipPolicyData, UserOperation, UserOperationReceipt } from "./types";
6
+ import { EncodedTxData, MetaTransaction, SponsorshipPolicyData, UserOperation, UserOperationReceipt } from "./types";
7
7
  export interface NearSafeConfig {
8
8
  accountId: string;
9
9
  mpcContractId: string;
@@ -149,6 +149,14 @@ export declare class NearSafe {
149
149
  * @returns {MetaTransaction} - A meta-transaction object for adding the new owner.
150
150
  */
151
151
  addOwnerTx(address: Address): MetaTransaction;
152
+ /**
153
+ * Creates a meta-transaction for adding a new owner to the Safe contract.
154
+ *
155
+ * @param {number} chainId - the chainId to build the transaction for.
156
+ * @param {Address} address - The address of the new owner to be removed.
157
+ * @returns {Promise<MetaTransaction>} - A meta-transaction object for adding the new owner.
158
+ */
159
+ removeOwnerTx(chainId: number, address: Address): Promise<MetaTransaction>;
152
160
  /**
153
161
  * Creates and returns a new `Erc4337Bundler` instance for the specified chain.
154
162
  *
@@ -156,13 +164,6 @@ export declare class NearSafe {
156
164
  * @returns {Erc4337Bundler} - A new instance of the `Erc4337Bundler` class configured for the specified chain.
157
165
  */
158
166
  private bundlerForChainId;
159
- /**
160
- * Decodes transaction data for a given EVM transaction and extracts relevant details.
161
- *
162
- * @param {EvmTransactionData} data - The raw transaction data to be decoded.
163
- * @returns {DecodedMultisend} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
164
- */
165
- decodeTxData(data: EvmTransactionData): DecodedMultisend;
166
167
  /**
167
168
  * Handles routing of signature requests based on the provided method, chain ID, and parameters.
168
169
  *
@@ -173,10 +174,7 @@ export declare class NearSafe {
173
174
  * - Returns a promise that resolves to an object containing the Ethereum Virtual Machine (EVM) message,
174
175
  * the payload (hashed data), and recovery data needed for reconstructing the signature request.
175
176
  */
176
- requestRouter({ method, chainId, params }: SignRequestData, sponsorshipPolicy?: string): Promise<{
177
- evmMessage: string;
178
- payload: number[];
179
- hash: Hash;
180
- }>;
177
+ requestRouter({ method, chainId, params }: SignRequestData, sponsorshipPolicy?: string): Promise<EncodedSignRequest>;
178
+ encodeForSafe(from: string): boolean;
181
179
  policyForChainId(chainId: number): Promise<SponsorshipPolicyData[]>;
182
180
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NearSafe = void 0;
4
- const ethers_multisend_1 = require("ethers-multisend");
5
4
  const near_ca_1 = require("near-ca");
6
5
  const viem_1 = require("viem");
7
6
  const constants_1 = require("./constants");
@@ -132,17 +131,17 @@ class NearSafe {
132
131
  * @returns {Promise<EncodedTxData>} - A promise that resolves to the encoded transaction data for the NEAR and EVM networks.
133
132
  */
134
133
  async encodeSignRequest(signRequest, sponsorshipPolicy) {
135
- const { payload, evmMessage, hash } = await this.requestRouter(signRequest, sponsorshipPolicy);
134
+ const { evmMessage, hashToSign } = await this.requestRouter(signRequest, sponsorshipPolicy);
136
135
  return {
137
136
  nearPayload: await this.nearAdapter.mpcContract.encodeSignatureRequestTx({
138
137
  path: this.nearAdapter.derivationPath,
139
- payload,
138
+ payload: (0, near_ca_1.toPayload)(hashToSign),
140
139
  key_version: 0,
141
140
  }),
142
141
  evmData: {
143
142
  chainId: signRequest.chainId,
144
- data: evmMessage,
145
- hash,
143
+ evmMessage,
144
+ hashToSign,
146
145
  },
147
146
  };
148
147
  }
@@ -231,6 +230,20 @@ class NearSafe {
231
230
  data: this.safePack.addOwnerData(address),
232
231
  };
233
232
  }
233
+ /**
234
+ * Creates a meta-transaction for adding a new owner to the Safe contract.
235
+ *
236
+ * @param {number} chainId - the chainId to build the transaction for.
237
+ * @param {Address} address - The address of the new owner to be removed.
238
+ * @returns {Promise<MetaTransaction>} - A meta-transaction object for adding the new owner.
239
+ */
240
+ async removeOwnerTx(chainId, address) {
241
+ return {
242
+ to: this.address,
243
+ value: "0",
244
+ data: await this.safePack.removeOwnerData(chainId, this.address, address),
245
+ };
246
+ }
234
247
  /**
235
248
  * Creates and returns a new `Erc4337Bundler` instance for the specified chain.
236
249
  *
@@ -240,54 +253,6 @@ class NearSafe {
240
253
  bundlerForChainId(chainId) {
241
254
  return new bundler_1.Erc4337Bundler(this.safePack.entryPoint.address, this.pimlicoKey, chainId);
242
255
  }
243
- /**
244
- * Decodes transaction data for a given EVM transaction and extracts relevant details.
245
- *
246
- * @param {EvmTransactionData} data - The raw transaction data to be decoded.
247
- * @returns {DecodedMultisend} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
248
- */
249
- decodeTxData(data) {
250
- try {
251
- const userOp = JSON.parse(data.data);
252
- const { callGasLimit, maxFeePerGas, maxPriorityFeePerGas } = userOp;
253
- const maxGasPrice = BigInt(maxFeePerGas) + BigInt(maxPriorityFeePerGas);
254
- const { args } = (0, viem_1.decodeFunctionData)({
255
- abi: this.safePack.m4337.abi,
256
- data: userOp.callData,
257
- });
258
- // Determine if singular or double!
259
- const transactions = (0, multisend_1.isMultisendTx)(args)
260
- ? (0, ethers_multisend_1.decodeMulti)(args[2])
261
- : [
262
- {
263
- to: args[0],
264
- value: args[1],
265
- data: args[2],
266
- operation: args[3],
267
- },
268
- ];
269
- return {
270
- chainId: data.chainId,
271
- // This is an upper bound on the gas fees (could be lower)
272
- costEstimate: (0, viem_1.formatEther)(BigInt(callGasLimit) * maxGasPrice),
273
- transactions,
274
- };
275
- }
276
- catch (error) {
277
- if (error instanceof SyntaxError) {
278
- return {
279
- chainId: data.chainId,
280
- costEstimate: "0",
281
- transactions: [],
282
- message: data.data,
283
- };
284
- }
285
- else {
286
- const message = error instanceof Error ? error.message : String(error);
287
- throw new Error(`decodeTxData: Unexpected error - ${message}`);
288
- }
289
- }
290
- }
291
256
  /**
292
257
  * Handles routing of signature requests based on the provided method, chain ID, and parameters.
293
258
  *
@@ -299,6 +264,27 @@ class NearSafe {
299
264
  * the payload (hashed data), and recovery data needed for reconstructing the signature request.
300
265
  */
301
266
  async requestRouter({ method, chainId, params }, sponsorshipPolicy) {
267
+ // Extract `from` based on the method and check uniqueness
268
+ const fromAddresses = (() => {
269
+ switch (method) {
270
+ case "eth_signTypedData":
271
+ case "eth_signTypedData_v4":
272
+ case "eth_sign":
273
+ return [params[0]];
274
+ case "personal_sign":
275
+ return [params[1]];
276
+ case "eth_sendTransaction":
277
+ return params.map((p) => p.from);
278
+ default:
279
+ return [];
280
+ }
281
+ })();
282
+ // Assert uniqueness
283
+ (0, util_1.assertUnique)(fromAddresses);
284
+ // Early return with eoaEncoding if `from` is not the Safe
285
+ if (!this.encodeForSafe(fromAddresses[0])) {
286
+ return (0, near_ca_1.requestRouter)({ method, chainId, params });
287
+ }
302
288
  const safeInfo = {
303
289
  address: { value: this.address },
304
290
  chainId: chainId.toString(),
@@ -314,23 +300,21 @@ class NearSafe {
314
300
  const [_, messageOrData] = params;
315
301
  const message = (0, safe_message_1.decodeSafeMessage)(messageOrData, safeInfo);
316
302
  return {
317
- evmMessage: message.safeMessageMessage,
318
- payload: (0, near_ca_1.toPayload)(message.safeMessageHash),
319
- hash: message.safeMessageHash,
303
+ evmMessage: message.decodedMessage,
304
+ hashToSign: message.safeMessageHash,
320
305
  };
321
306
  }
322
307
  case "personal_sign": {
323
308
  const [messageHash, _] = params;
324
309
  const message = (0, safe_message_1.decodeSafeMessage)(messageHash, safeInfo);
325
310
  return {
326
- // TODO(bh2smith) this is a bit of a hack.
327
311
  evmMessage: message.decodedMessage,
328
- payload: (0, near_ca_1.toPayload)(message.safeMessageHash),
329
- hash: message.safeMessageHash,
312
+ hashToSign: message.safeMessageHash,
330
313
  };
331
314
  }
332
315
  case "eth_sendTransaction": {
333
- const transactions = (0, util_1.metaTransactionsFromRequest)(params);
316
+ const castParams = params;
317
+ const transactions = (0, util_1.metaTransactionsFromRequest)(castParams);
334
318
  const userOp = await this.buildTransaction({
335
319
  chainId,
336
320
  transactions,
@@ -338,13 +322,21 @@ class NearSafe {
338
322
  });
339
323
  const opHash = await this.opHash(chainId, userOp);
340
324
  return {
341
- payload: (0, near_ca_1.toPayload)(opHash),
342
325
  evmMessage: JSON.stringify(userOp),
343
- hash: await this.opHash(chainId, userOp),
326
+ hashToSign: opHash,
344
327
  };
345
328
  }
346
329
  }
347
330
  }
331
+ encodeForSafe(from) {
332
+ const fromLower = from.toLowerCase();
333
+ if (![this.address, this.mpcAddress]
334
+ .map((t) => t.toLowerCase())
335
+ .includes(fromLower)) {
336
+ throw new Error(`Unexpected from address ${from}`);
337
+ }
338
+ return this.address.toLowerCase() === fromLower;
339
+ }
348
340
  async policyForChainId(chainId) {
349
341
  const bundler = this.bundlerForChainId(chainId);
350
342
  return bundler.getSponsorshipPolicies();
@@ -1,4 +1,4 @@
1
- import { FunctionCallTransaction, SignArgs } from "near-ca";
1
+ import { EIP712TypedData, EncodedSignRequest, FunctionCallTransaction, SignArgs } from "near-ca";
2
2
  import { Address, Hex, ParseAbi } from "viem";
3
3
  /**
4
4
  * Represents a collection of Safe contract deployments, each with its own address and ABI.
@@ -212,20 +212,15 @@ export interface MetaTransaction {
212
212
  readonly operation?: OperationType;
213
213
  }
214
214
  /**
215
- * Represents raw transaction data for an EVM transaction.
215
+ * Extends EncodedSignRequest to include a chain ID for cross-chain compatibility.
216
216
  */
217
- export interface EvmTransactionData {
218
- /** The chain ID of the network where the transaction is being executed. */
217
+ export interface SafeEncodedSignRequest extends EncodedSignRequest {
219
218
  chainId: number;
220
- /** The raw data of the transaction. */
221
- data: string;
222
- /** The hash of the transaction. */
223
- hash: string;
224
219
  }
225
220
  /**
226
221
  * Represents the decoded details of a multisend transaction.
227
222
  */
228
- export interface DecodedMultisend {
223
+ export interface DecodedTxData {
229
224
  /** The chain ID of the network where the multisend transaction is being executed. */
230
225
  chainId: number;
231
226
  /** The estimated cost of the multisend transaction in Ether.
@@ -235,14 +230,14 @@ export interface DecodedMultisend {
235
230
  /** The list of meta-transactions included in the multisend. */
236
231
  transactions: MetaTransaction[];
237
232
  /** Raw Message to sign if no transactions present. */
238
- message?: string;
233
+ message?: string | EIP712TypedData;
239
234
  }
240
235
  /**
241
236
  * Represents encoded transaction data for both NEAR and EVM networks.
242
237
  */
243
238
  export interface EncodedTxData {
244
239
  /** The encoded transaction data for the EVM network. */
245
- evmData: EvmTransactionData;
240
+ evmData: SafeEncodedSignRequest;
246
241
  /** The encoded payload for a NEAR function call, including the signing arguments. */
247
242
  nearPayload: FunctionCallTransaction<{
248
243
  request: SignArgs;
@@ -37,4 +37,5 @@ export declare function signatureFromTxHash(txHash: string, accountId?: string):
37
37
  * @throws Will throw an error if all promises reject with the message "All promises rejected".
38
38
  */
39
39
  export declare function raceToFirstResolve<T>(promises: Promise<T>[]): Promise<T>;
40
+ export declare function assertUnique<T>(iterable: Iterable<T>, errorMessage?: string): void;
40
41
  export {};
package/dist/cjs/util.js CHANGED
@@ -10,6 +10,7 @@ exports.metaTransactionsFromRequest = metaTransactionsFromRequest;
10
10
  exports.saltNonceFromMessage = saltNonceFromMessage;
11
11
  exports.signatureFromTxHash = signatureFromTxHash;
12
12
  exports.raceToFirstResolve = raceToFirstResolve;
13
+ exports.assertUnique = assertUnique;
13
14
  const near_ca_1 = require("near-ca");
14
15
  const viem_1 = require("viem");
15
16
  exports.PLACEHOLDER_SIG = (0, viem_1.encodePacked)(["uint48", "uint48"], [0, 0]);
@@ -40,6 +41,7 @@ function getClient(chainId) {
40
41
  function metaTransactionsFromRequest(params) {
41
42
  let transactions;
42
43
  if ((0, viem_1.isHex)(params)) {
44
+ // TODO: Consider deprecating this route.
43
45
  // If RLP hex is given, decode the transaction and build EthTransactionParams
44
46
  const tx = (0, viem_1.parseTransaction)(params);
45
47
  transactions = [
@@ -52,6 +54,7 @@ function metaTransactionsFromRequest(params) {
52
54
  ];
53
55
  }
54
56
  else {
57
+ // TODO: add type guard here.
55
58
  transactions = params;
56
59
  }
57
60
  return transactions.map((tx) => ({
@@ -130,3 +133,9 @@ async function raceToFirstResolve(promises) {
130
133
  });
131
134
  });
132
135
  }
136
+ function assertUnique(iterable, errorMessage = "The collection contains more than one distinct element.") {
137
+ const uniqueValues = new Set(iterable);
138
+ if (uniqueValues.size > 1) {
139
+ throw new Error(errorMessage);
140
+ }
141
+ }
@@ -1,2 +1,3 @@
1
1
  export declare const USER_OP_IDENTIFIER: `0x${string}`;
2
2
  export declare const DEFAULT_SAFE_SALT_NONCE: string;
3
+ export declare const SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001";
@@ -4,3 +4,4 @@ const DOMAIN_SEPARATOR = "bitte/near-safe";
4
4
  export const USER_OP_IDENTIFIER = toHex(DOMAIN_SEPARATOR, { size: 16 });
5
5
  // 130811896738364114529934864114944206080
6
6
  export const DEFAULT_SAFE_SALT_NONCE = BigInt(USER_OP_IDENTIFIER).toString();
7
+ export const SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001";
@@ -0,0 +1,14 @@
1
+ import { EIP712TypedData } from "near-ca";
2
+ import { Hex, TransactionSerializable } from "viem";
3
+ import { DecodedTxData, SafeEncodedSignRequest, UserOperation } from "./types";
4
+ /**
5
+ * Decodes transaction data for a given EVM transaction and extracts relevant details.
6
+ *
7
+ * @param {EvmTransactionData} data - The raw transaction data to be decoded.
8
+ * @returns {DecodedTxData} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
9
+ */
10
+ export declare function decodeTxData({ evmMessage, chainId, }: SafeEncodedSignRequest): DecodedTxData;
11
+ export declare function decodeTransactionSerializable(chainId: number, tx: TransactionSerializable): DecodedTxData;
12
+ export declare function decodeRlpHex(chainId: number, tx: Hex): DecodedTxData;
13
+ export declare function decodeTypedData(chainId: number, data: EIP712TypedData): DecodedTxData;
14
+ export declare function decodeUserOperation(chainId: number, userOp: UserOperation): DecodedTxData;
@@ -0,0 +1,103 @@
1
+ import { decodeMulti } from "ethers-multisend";
2
+ import { decodeFunctionData, formatEther, parseTransaction, serializeTransaction, } from "viem";
3
+ import { SAFE_DEPLOYMENTS } from "./_gen/deployments";
4
+ import { isMultisendTx } from "./lib/multisend";
5
+ import { isRlpHex, isTransactionSerializable } from "./lib/safe-message";
6
+ /**
7
+ * Decodes transaction data for a given EVM transaction and extracts relevant details.
8
+ *
9
+ * @param {EvmTransactionData} data - The raw transaction data to be decoded.
10
+ * @returns {DecodedTxData} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
11
+ */
12
+ export function decodeTxData({ evmMessage, chainId, }) {
13
+ const data = evmMessage;
14
+ if (isRlpHex(evmMessage)) {
15
+ decodeRlpHex(chainId, evmMessage);
16
+ }
17
+ if (isTransactionSerializable(data)) {
18
+ return decodeTransactionSerializable(chainId, data);
19
+ }
20
+ if (typeof data !== "string") {
21
+ return decodeTypedData(chainId, data);
22
+ }
23
+ try {
24
+ // Stringified UserOperation.
25
+ const userOp = JSON.parse(data);
26
+ return decodeUserOperation(chainId, userOp);
27
+ }
28
+ catch (error) {
29
+ if (error instanceof SyntaxError) {
30
+ // Raw message string.
31
+ return {
32
+ chainId,
33
+ costEstimate: "0",
34
+ transactions: [],
35
+ message: data,
36
+ };
37
+ }
38
+ else {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ throw new Error(`decodeTxData: Unexpected error - ${message}`);
41
+ }
42
+ }
43
+ }
44
+ export function decodeTransactionSerializable(chainId, tx) {
45
+ const { gas, maxFeePerGas, maxPriorityFeePerGas, to } = tx;
46
+ if (chainId !== tx.chainId) {
47
+ throw Error(`Transaction chainId mismatch ${chainId} != ${tx.chainId}`);
48
+ }
49
+ if (!gas || !maxFeePerGas || !maxPriorityFeePerGas) {
50
+ throw Error(`Insufficient feeData for ${serializeTransaction(tx)}. Check https://rawtxdecode.in/`);
51
+ }
52
+ if (!to) {
53
+ throw Error(`Transaction is missing the 'to' in ${serializeTransaction(tx)}. Check https://rawtxdecode.in/`);
54
+ }
55
+ return {
56
+ chainId,
57
+ // This is an upper bound on the gas fees (could be lower)
58
+ costEstimate: formatEther(gas * (maxFeePerGas + maxPriorityFeePerGas)),
59
+ transactions: [
60
+ {
61
+ to,
62
+ value: (tx.value || 0n).toString(),
63
+ data: tx.data || "0x",
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ export function decodeRlpHex(chainId, tx) {
69
+ return decodeTransactionSerializable(chainId, parseTransaction(tx));
70
+ }
71
+ export function decodeTypedData(chainId, data) {
72
+ return {
73
+ chainId,
74
+ costEstimate: "0",
75
+ transactions: [],
76
+ message: data,
77
+ };
78
+ }
79
+ export function decodeUserOperation(chainId, userOp) {
80
+ const { callGasLimit, maxFeePerGas, maxPriorityFeePerGas } = userOp;
81
+ const maxGasPrice = BigInt(maxFeePerGas) + BigInt(maxPriorityFeePerGas);
82
+ const { args } = decodeFunctionData({
83
+ abi: SAFE_DEPLOYMENTS.m4337.abi,
84
+ data: userOp.callData,
85
+ });
86
+ // Determine if singular or double!
87
+ const transactions = isMultisendTx(args)
88
+ ? decodeMulti(args[2])
89
+ : [
90
+ {
91
+ to: args[0],
92
+ value: args[1],
93
+ data: args[2],
94
+ operation: args[3],
95
+ },
96
+ ];
97
+ return {
98
+ chainId,
99
+ // This is an upper bound on the gas fees (could be lower)
100
+ costEstimate: formatEther(BigInt(callGasLimit) * maxGasPrice),
101
+ transactions,
102
+ };
103
+ }
@@ -1,6 +1,6 @@
1
1
  import { type SafeInfo } from "@safe-global/safe-gateway-typescript-sdk";
2
2
  import { EIP712TypedData } from "near-ca";
3
- import { Hash } from "viem";
3
+ import { Hash, Hex, TransactionSerializable } from "viem";
4
4
  export type DecodedSafeMessage = {
5
5
  decodedMessage: string | EIP712TypedData;
6
6
  safeMessageMessage: string;
@@ -20,3 +20,5 @@ export type MinimalSafeInfo = Pick<SafeInfo, "address" | "version" | "chainId">;
20
20
  * }`
21
21
  */
22
22
  export declare function decodeSafeMessage(message: string | EIP712TypedData, safe: MinimalSafeInfo): DecodedSafeMessage;
23
+ export declare function isTransactionSerializable(data: unknown): data is TransactionSerializable;
24
+ export declare function isRlpHex(data: unknown): data is Hex;
@@ -1,5 +1,5 @@
1
1
  import { gte } from "semver";
2
- import { fromHex, hashMessage, hashTypedData, isHex, } from "viem";
2
+ import { fromHex, hashMessage, hashTypedData, isHex, parseTransaction, serializeTransaction, } from "viem";
3
3
  /*
4
4
  * From v1.3.0, EIP-1271 support was moved to the CompatibilityFallbackHandler.
5
5
  * Also 1.3.0 introduces the chainId in the domain part of the SafeMessage
@@ -90,3 +90,22 @@ export function decodeSafeMessage(message, safe) {
90
90
  // };
91
91
  // export const isBlindSigningPayload = (obj: EIP712TypedData | string): boolean =>
92
92
  // !isEIP712TypedData(obj) && isHash(obj);
93
+ // Cheeky attempt to serialize. return true if successful!
94
+ export function isTransactionSerializable(data) {
95
+ try {
96
+ serializeTransaction(data);
97
+ return true;
98
+ }
99
+ catch (error) {
100
+ return false;
101
+ }
102
+ }
103
+ export function isRlpHex(data) {
104
+ try {
105
+ parseTransaction(data);
106
+ return true;
107
+ }
108
+ catch (error) {
109
+ return false;
110
+ }
111
+ }
@@ -14,8 +14,10 @@ export declare class SafeContractSuite {
14
14
  addressForSetup(setup: Hex, saltNonce: string): Promise<Address>;
15
15
  getSetup(owners: string[]): Hex;
16
16
  addOwnerData(newOwner: Address): Hex;
17
+ removeOwnerData(chainId: number, safeAddress: Address, owner: Address): Promise<Hex>;
17
18
  getOpHash(chainId: number, unsignedUserOp: UserOperation): Promise<Hash>;
18
19
  private factoryDataForSetup;
19
20
  buildUserOp(nonce: bigint, txData: MetaTransaction, safeAddress: Address, feeData: GasPrice, setup: string, safeNotDeployed: boolean, safeSaltNonce: string): Promise<UnsignedUserOperation>;
20
21
  getNonce(address: Address, chainId: number): Promise<bigint>;
22
+ prevOwner(chainId: number, safeAddress: Address, owner: Address): Promise<Address>;
21
23
  }
@@ -1,6 +1,6 @@
1
- import { concat, encodeFunctionData, encodePacked, getAddress, getCreate2Address, keccak256, toHex, zeroAddress, } from "viem";
1
+ import { concat, encodeFunctionData, encodePacked, getAddress, getCreate2Address, keccak256, parseAbi, toHex, zeroAddress, } from "viem";
2
2
  import { SAFE_DEPLOYMENTS } from "../_gen/deployments";
3
- import { USER_OP_IDENTIFIER } from "../constants";
3
+ import { SENTINEL_OWNERS, USER_OP_IDENTIFIER } from "../constants";
4
4
  import { PLACEHOLDER_SIG, getClient, packGas, packPaymasterData, } from "../util";
5
5
  /**
6
6
  * All contracts used in account creation & execution
@@ -69,6 +69,15 @@ export class SafeContractSuite {
69
69
  args: [newOwner, 1],
70
70
  });
71
71
  }
72
+ async removeOwnerData(chainId, safeAddress, owner) {
73
+ const prevOwner = await this.prevOwner(chainId, safeAddress, owner);
74
+ return encodeFunctionData({
75
+ abi: this.singleton.abi,
76
+ functionName: "removeOwner",
77
+ // Keep threshold at 1!
78
+ args: [prevOwner, owner, 1],
79
+ });
80
+ }
72
81
  async getOpHash(chainId, unsignedUserOp) {
73
82
  const { factory, factoryData, verificationGasLimit, callGasLimit, maxPriorityFeePerGas, maxFeePerGas, } = unsignedUserOp;
74
83
  const client = await getClient(chainId);
@@ -135,4 +144,20 @@ export class SafeContractSuite {
135
144
  }));
136
145
  return nonce;
137
146
  }
147
+ async prevOwner(chainId, safeAddress, owner) {
148
+ const client = getClient(chainId);
149
+ const currentOwners = await client.readContract({
150
+ address: safeAddress,
151
+ // abi: this.singleton.abi,
152
+ abi: parseAbi([
153
+ "function getOwners() public view returns (address[] memory)",
154
+ ]),
155
+ functionName: "getOwners",
156
+ });
157
+ const ownerIndex = currentOwners.findIndex((t) => t === owner);
158
+ if (ownerIndex === -1) {
159
+ throw new Error(`Not a current owner: ${owner}`);
160
+ }
161
+ return ownerIndex > 0 ? currentOwners[ownerIndex - 1] : SENTINEL_OWNERS;
162
+ }
138
163
  }
@@ -1,9 +1,9 @@
1
1
  import { NearConfig } from "near-api-js/lib/near";
2
2
  import { FinalExecutionOutcome } from "near-api-js/lib/providers";
3
- import { NearEthAdapter, SignRequestData } from "near-ca";
3
+ import { NearEthAdapter, SignRequestData, EncodedSignRequest } from "near-ca";
4
4
  import { Address, Hash, Hex } from "viem";
5
5
  import { SafeContractSuite } from "./lib/safe";
6
- import { DecodedMultisend, EncodedTxData, EvmTransactionData, MetaTransaction, SponsorshipPolicyData, UserOperation, UserOperationReceipt } from "./types";
6
+ import { EncodedTxData, MetaTransaction, SponsorshipPolicyData, UserOperation, UserOperationReceipt } from "./types";
7
7
  export interface NearSafeConfig {
8
8
  accountId: string;
9
9
  mpcContractId: string;
@@ -149,6 +149,14 @@ export declare class NearSafe {
149
149
  * @returns {MetaTransaction} - A meta-transaction object for adding the new owner.
150
150
  */
151
151
  addOwnerTx(address: Address): MetaTransaction;
152
+ /**
153
+ * Creates a meta-transaction for adding a new owner to the Safe contract.
154
+ *
155
+ * @param {number} chainId - the chainId to build the transaction for.
156
+ * @param {Address} address - The address of the new owner to be removed.
157
+ * @returns {Promise<MetaTransaction>} - A meta-transaction object for adding the new owner.
158
+ */
159
+ removeOwnerTx(chainId: number, address: Address): Promise<MetaTransaction>;
152
160
  /**
153
161
  * Creates and returns a new `Erc4337Bundler` instance for the specified chain.
154
162
  *
@@ -156,13 +164,6 @@ export declare class NearSafe {
156
164
  * @returns {Erc4337Bundler} - A new instance of the `Erc4337Bundler` class configured for the specified chain.
157
165
  */
158
166
  private bundlerForChainId;
159
- /**
160
- * Decodes transaction data for a given EVM transaction and extracts relevant details.
161
- *
162
- * @param {EvmTransactionData} data - The raw transaction data to be decoded.
163
- * @returns {DecodedMultisend} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
164
- */
165
- decodeTxData(data: EvmTransactionData): DecodedMultisend;
166
167
  /**
167
168
  * Handles routing of signature requests based on the provided method, chain ID, and parameters.
168
169
  *
@@ -173,10 +174,7 @@ export declare class NearSafe {
173
174
  * - Returns a promise that resolves to an object containing the Ethereum Virtual Machine (EVM) message,
174
175
  * the payload (hashed data), and recovery data needed for reconstructing the signature request.
175
176
  */
176
- requestRouter({ method, chainId, params }: SignRequestData, sponsorshipPolicy?: string): Promise<{
177
- evmMessage: string;
178
- payload: number[];
179
- hash: Hash;
180
- }>;
177
+ requestRouter({ method, chainId, params }: SignRequestData, sponsorshipPolicy?: string): Promise<EncodedSignRequest>;
178
+ encodeForSafe(from: string): boolean;
181
179
  policyForChainId(chainId: number): Promise<SponsorshipPolicyData[]>;
182
180
  }
@@ -1,12 +1,11 @@
1
- import { decodeMulti } from "ethers-multisend";
2
- import { setupAdapter, signatureFromOutcome, toPayload, } from "near-ca";
3
- import { decodeFunctionData, formatEther, serializeSignature, } from "viem";
1
+ import { setupAdapter, signatureFromOutcome, toPayload, requestRouter as mpcRequestRouter, } from "near-ca";
2
+ import { serializeSignature } from "viem";
4
3
  import { DEFAULT_SAFE_SALT_NONCE } from "./constants";
5
4
  import { Erc4337Bundler } from "./lib/bundler";
6
- import { encodeMulti, isMultisendTx } from "./lib/multisend";
5
+ import { encodeMulti } from "./lib/multisend";
7
6
  import { SafeContractSuite } from "./lib/safe";
8
7
  import { decodeSafeMessage } from "./lib/safe-message";
9
- import { getClient, isContract, metaTransactionsFromRequest, packSignature, } from "./util";
8
+ import { assertUnique, getClient, isContract, metaTransactionsFromRequest, packSignature, } from "./util";
10
9
  export class NearSafe {
11
10
  nearAdapter;
12
11
  address;
@@ -135,17 +134,17 @@ export class NearSafe {
135
134
  * @returns {Promise<EncodedTxData>} - A promise that resolves to the encoded transaction data for the NEAR and EVM networks.
136
135
  */
137
136
  async encodeSignRequest(signRequest, sponsorshipPolicy) {
138
- const { payload, evmMessage, hash } = await this.requestRouter(signRequest, sponsorshipPolicy);
137
+ const { evmMessage, hashToSign } = await this.requestRouter(signRequest, sponsorshipPolicy);
139
138
  return {
140
139
  nearPayload: await this.nearAdapter.mpcContract.encodeSignatureRequestTx({
141
140
  path: this.nearAdapter.derivationPath,
142
- payload,
141
+ payload: toPayload(hashToSign),
143
142
  key_version: 0,
144
143
  }),
145
144
  evmData: {
146
145
  chainId: signRequest.chainId,
147
- data: evmMessage,
148
- hash,
146
+ evmMessage,
147
+ hashToSign,
149
148
  },
150
149
  };
151
150
  }
@@ -234,6 +233,20 @@ export class NearSafe {
234
233
  data: this.safePack.addOwnerData(address),
235
234
  };
236
235
  }
236
+ /**
237
+ * Creates a meta-transaction for adding a new owner to the Safe contract.
238
+ *
239
+ * @param {number} chainId - the chainId to build the transaction for.
240
+ * @param {Address} address - The address of the new owner to be removed.
241
+ * @returns {Promise<MetaTransaction>} - A meta-transaction object for adding the new owner.
242
+ */
243
+ async removeOwnerTx(chainId, address) {
244
+ return {
245
+ to: this.address,
246
+ value: "0",
247
+ data: await this.safePack.removeOwnerData(chainId, this.address, address),
248
+ };
249
+ }
237
250
  /**
238
251
  * Creates and returns a new `Erc4337Bundler` instance for the specified chain.
239
252
  *
@@ -243,54 +256,6 @@ export class NearSafe {
243
256
  bundlerForChainId(chainId) {
244
257
  return new Erc4337Bundler(this.safePack.entryPoint.address, this.pimlicoKey, chainId);
245
258
  }
246
- /**
247
- * Decodes transaction data for a given EVM transaction and extracts relevant details.
248
- *
249
- * @param {EvmTransactionData} data - The raw transaction data to be decoded.
250
- * @returns {DecodedMultisend} - An object containing the chain ID, estimated cost, and a list of decoded meta-transactions.
251
- */
252
- decodeTxData(data) {
253
- try {
254
- const userOp = JSON.parse(data.data);
255
- const { callGasLimit, maxFeePerGas, maxPriorityFeePerGas } = userOp;
256
- const maxGasPrice = BigInt(maxFeePerGas) + BigInt(maxPriorityFeePerGas);
257
- const { args } = decodeFunctionData({
258
- abi: this.safePack.m4337.abi,
259
- data: userOp.callData,
260
- });
261
- // Determine if singular or double!
262
- const transactions = isMultisendTx(args)
263
- ? decodeMulti(args[2])
264
- : [
265
- {
266
- to: args[0],
267
- value: args[1],
268
- data: args[2],
269
- operation: args[3],
270
- },
271
- ];
272
- return {
273
- chainId: data.chainId,
274
- // This is an upper bound on the gas fees (could be lower)
275
- costEstimate: formatEther(BigInt(callGasLimit) * maxGasPrice),
276
- transactions,
277
- };
278
- }
279
- catch (error) {
280
- if (error instanceof SyntaxError) {
281
- return {
282
- chainId: data.chainId,
283
- costEstimate: "0",
284
- transactions: [],
285
- message: data.data,
286
- };
287
- }
288
- else {
289
- const message = error instanceof Error ? error.message : String(error);
290
- throw new Error(`decodeTxData: Unexpected error - ${message}`);
291
- }
292
- }
293
- }
294
259
  /**
295
260
  * Handles routing of signature requests based on the provided method, chain ID, and parameters.
296
261
  *
@@ -302,6 +267,27 @@ export class NearSafe {
302
267
  * the payload (hashed data), and recovery data needed for reconstructing the signature request.
303
268
  */
304
269
  async requestRouter({ method, chainId, params }, sponsorshipPolicy) {
270
+ // Extract `from` based on the method and check uniqueness
271
+ const fromAddresses = (() => {
272
+ switch (method) {
273
+ case "eth_signTypedData":
274
+ case "eth_signTypedData_v4":
275
+ case "eth_sign":
276
+ return [params[0]];
277
+ case "personal_sign":
278
+ return [params[1]];
279
+ case "eth_sendTransaction":
280
+ return params.map((p) => p.from);
281
+ default:
282
+ return [];
283
+ }
284
+ })();
285
+ // Assert uniqueness
286
+ assertUnique(fromAddresses);
287
+ // Early return with eoaEncoding if `from` is not the Safe
288
+ if (!this.encodeForSafe(fromAddresses[0])) {
289
+ return mpcRequestRouter({ method, chainId, params });
290
+ }
305
291
  const safeInfo = {
306
292
  address: { value: this.address },
307
293
  chainId: chainId.toString(),
@@ -317,23 +303,21 @@ export class NearSafe {
317
303
  const [_, messageOrData] = params;
318
304
  const message = decodeSafeMessage(messageOrData, safeInfo);
319
305
  return {
320
- evmMessage: message.safeMessageMessage,
321
- payload: toPayload(message.safeMessageHash),
322
- hash: message.safeMessageHash,
306
+ evmMessage: message.decodedMessage,
307
+ hashToSign: message.safeMessageHash,
323
308
  };
324
309
  }
325
310
  case "personal_sign": {
326
311
  const [messageHash, _] = params;
327
312
  const message = decodeSafeMessage(messageHash, safeInfo);
328
313
  return {
329
- // TODO(bh2smith) this is a bit of a hack.
330
314
  evmMessage: message.decodedMessage,
331
- payload: toPayload(message.safeMessageHash),
332
- hash: message.safeMessageHash,
315
+ hashToSign: message.safeMessageHash,
333
316
  };
334
317
  }
335
318
  case "eth_sendTransaction": {
336
- const transactions = metaTransactionsFromRequest(params);
319
+ const castParams = params;
320
+ const transactions = metaTransactionsFromRequest(castParams);
337
321
  const userOp = await this.buildTransaction({
338
322
  chainId,
339
323
  transactions,
@@ -341,13 +325,21 @@ export class NearSafe {
341
325
  });
342
326
  const opHash = await this.opHash(chainId, userOp);
343
327
  return {
344
- payload: toPayload(opHash),
345
328
  evmMessage: JSON.stringify(userOp),
346
- hash: await this.opHash(chainId, userOp),
329
+ hashToSign: opHash,
347
330
  };
348
331
  }
349
332
  }
350
333
  }
334
+ encodeForSafe(from) {
335
+ const fromLower = from.toLowerCase();
336
+ if (![this.address, this.mpcAddress]
337
+ .map((t) => t.toLowerCase())
338
+ .includes(fromLower)) {
339
+ throw new Error(`Unexpected from address ${from}`);
340
+ }
341
+ return this.address.toLowerCase() === fromLower;
342
+ }
351
343
  async policyForChainId(chainId) {
352
344
  const bundler = this.bundlerForChainId(chainId);
353
345
  return bundler.getSponsorshipPolicies();
@@ -1,4 +1,4 @@
1
- import { FunctionCallTransaction, SignArgs } from "near-ca";
1
+ import { EIP712TypedData, EncodedSignRequest, FunctionCallTransaction, SignArgs } from "near-ca";
2
2
  import { Address, Hex, ParseAbi } from "viem";
3
3
  /**
4
4
  * Represents a collection of Safe contract deployments, each with its own address and ABI.
@@ -212,20 +212,15 @@ export interface MetaTransaction {
212
212
  readonly operation?: OperationType;
213
213
  }
214
214
  /**
215
- * Represents raw transaction data for an EVM transaction.
215
+ * Extends EncodedSignRequest to include a chain ID for cross-chain compatibility.
216
216
  */
217
- export interface EvmTransactionData {
218
- /** The chain ID of the network where the transaction is being executed. */
217
+ export interface SafeEncodedSignRequest extends EncodedSignRequest {
219
218
  chainId: number;
220
- /** The raw data of the transaction. */
221
- data: string;
222
- /** The hash of the transaction. */
223
- hash: string;
224
219
  }
225
220
  /**
226
221
  * Represents the decoded details of a multisend transaction.
227
222
  */
228
- export interface DecodedMultisend {
223
+ export interface DecodedTxData {
229
224
  /** The chain ID of the network where the multisend transaction is being executed. */
230
225
  chainId: number;
231
226
  /** The estimated cost of the multisend transaction in Ether.
@@ -235,14 +230,14 @@ export interface DecodedMultisend {
235
230
  /** The list of meta-transactions included in the multisend. */
236
231
  transactions: MetaTransaction[];
237
232
  /** Raw Message to sign if no transactions present. */
238
- message?: string;
233
+ message?: string | EIP712TypedData;
239
234
  }
240
235
  /**
241
236
  * Represents encoded transaction data for both NEAR and EVM networks.
242
237
  */
243
238
  export interface EncodedTxData {
244
239
  /** The encoded transaction data for the EVM network. */
245
- evmData: EvmTransactionData;
240
+ evmData: SafeEncodedSignRequest;
246
241
  /** The encoded payload for a NEAR function call, including the signing arguments. */
247
242
  nearPayload: FunctionCallTransaction<{
248
243
  request: SignArgs;
@@ -37,4 +37,5 @@ export declare function signatureFromTxHash(txHash: string, accountId?: string):
37
37
  * @throws Will throw an error if all promises reject with the message "All promises rejected".
38
38
  */
39
39
  export declare function raceToFirstResolve<T>(promises: Promise<T>[]): Promise<T>;
40
+ export declare function assertUnique<T>(iterable: Iterable<T>, errorMessage?: string): void;
40
41
  export {};
package/dist/esm/util.js CHANGED
@@ -27,6 +27,7 @@ export function getClient(chainId) {
27
27
  export function metaTransactionsFromRequest(params) {
28
28
  let transactions;
29
29
  if (isHex(params)) {
30
+ // TODO: Consider deprecating this route.
30
31
  // If RLP hex is given, decode the transaction and build EthTransactionParams
31
32
  const tx = parseTransaction(params);
32
33
  transactions = [
@@ -39,6 +40,7 @@ export function metaTransactionsFromRequest(params) {
39
40
  ];
40
41
  }
41
42
  else {
43
+ // TODO: add type guard here.
42
44
  transactions = params;
43
45
  }
44
46
  return transactions.map((tx) => ({
@@ -117,3 +119,9 @@ export async function raceToFirstResolve(promises) {
117
119
  });
118
120
  });
119
121
  }
122
+ export function assertUnique(iterable, errorMessage = "The collection contains more than one distinct element.") {
123
+ const uniqueValues = new Set(iterable);
124
+ if (uniqueValues.size > 1) {
125
+ throw new Error(errorMessage);
126
+ }
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "near-safe",
3
- "version": "0.7.6",
3
+ "version": "0.8.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",
@@ -44,7 +44,7 @@
44
44
  "@safe-global/safe-gateway-typescript-sdk": "^3.22.2",
45
45
  "ethers-multisend": "^3.1.0",
46
46
  "near-api-js": "^5.0.0",
47
- "near-ca": "^0.5.12",
47
+ "near-ca": "^0.6.0",
48
48
  "semver": "^7.6.3",
49
49
  "viem": "^2.16.5"
50
50
  },