near-safe 0.7.5 → 0.8.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.
@@ -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.5",
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.10",
47
+ "near-ca": "^0.6.0",
48
48
  "semver": "^7.6.3",
49
49
  "viem": "^2.16.5"
50
50
  },