@marigoldlabs/web3-tester 0.4.1 → 0.4.2

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.
Files changed (59) hide show
  1. package/README.md +317 -11
  2. package/dist/anvil.d.ts.map +1 -1
  3. package/dist/anvil.js.map +1 -1
  4. package/dist/benchmark.d.ts +55 -0
  5. package/dist/benchmark.d.ts.map +1 -0
  6. package/dist/benchmark.js +168 -0
  7. package/dist/benchmark.js.map +1 -0
  8. package/dist/fixtures.js +2 -2
  9. package/dist/fixtures.js.map +1 -1
  10. package/dist/index.d.ts +13 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/injected-provider.d.ts.map +1 -1
  15. package/dist/injected-provider.js +748 -25
  16. package/dist/injected-provider.js.map +1 -1
  17. package/dist/mock-wallet-controller.d.ts +150 -2
  18. package/dist/mock-wallet-controller.d.ts.map +1 -1
  19. package/dist/mock-wallet-controller.js +613 -24
  20. package/dist/mock-wallet-controller.js.map +1 -1
  21. package/dist/private-key-rpc-client.d.ts +144 -132
  22. package/dist/private-key-rpc-client.d.ts.map +1 -1
  23. package/dist/private-key-rpc-client.js +142 -20
  24. package/dist/private-key-rpc-client.js.map +1 -1
  25. package/dist/real-wallet-cache.d.ts +38 -0
  26. package/dist/real-wallet-cache.d.ts.map +1 -1
  27. package/dist/real-wallet-cache.js +143 -57
  28. package/dist/real-wallet-cache.js.map +1 -1
  29. package/dist/real-wallet-extension-fixtures.d.ts +35 -0
  30. package/dist/real-wallet-extension-fixtures.d.ts.map +1 -0
  31. package/dist/real-wallet-extension-fixtures.js +96 -0
  32. package/dist/real-wallet-extension-fixtures.js.map +1 -0
  33. package/dist/real-wallet-extension.d.ts +86 -0
  34. package/dist/real-wallet-extension.d.ts.map +1 -0
  35. package/dist/real-wallet-extension.js +245 -0
  36. package/dist/real-wallet-extension.js.map +1 -0
  37. package/dist/real-wallet-fixtures.d.ts.map +1 -1
  38. package/dist/real-wallet-fixtures.js +46 -31
  39. package/dist/real-wallet-fixtures.js.map +1 -1
  40. package/dist/real-wallet.d.ts +5 -14
  41. package/dist/real-wallet.d.ts.map +1 -1
  42. package/dist/real-wallet.js +668 -435
  43. package/dist/real-wallet.js.map +1 -1
  44. package/dist/safe.d.ts +311 -0
  45. package/dist/safe.d.ts.map +1 -0
  46. package/dist/safe.js +743 -0
  47. package/dist/safe.js.map +1 -0
  48. package/dist/types.d.ts +35 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/wallet-personas.d.ts +99 -0
  51. package/dist/wallet-personas.d.ts.map +1 -0
  52. package/dist/wallet-personas.js +666 -0
  53. package/dist/wallet-personas.js.map +1 -0
  54. package/dist/walletconnect.d.ts +176 -9
  55. package/dist/walletconnect.d.ts.map +1 -1
  56. package/dist/walletconnect.js +514 -74
  57. package/dist/walletconnect.js.map +1 -1
  58. package/examples/playwright.config.ts +8 -0
  59. package/package.json +29 -3
package/dist/safe.js ADDED
@@ -0,0 +1,743 @@
1
+ import { encodeAbiParameters, hashTypedData, isAddress, keccak256, toHex, } from 'viem';
2
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
3
+ const SAFE_APP_BRIDGE_BINDING = '__web3TesterSafeAppBridge';
4
+ export const SAFE_MULTISEND_CALL_ONLY_ADDRESS = '0x9641d764fc13c8b624c04430c7356c1c7c8102e2';
5
+ const SAFE_MULTISEND_SELECTOR = keccak256(toHex('multiSend(bytes)')).slice(0, 10);
6
+ export const SAFE_TRANSACTION_TYPED_DATA_TYPES = {
7
+ SafeTx: [
8
+ { name: 'to', type: 'address' },
9
+ { name: 'value', type: 'uint256' },
10
+ { name: 'data', type: 'bytes' },
11
+ { name: 'operation', type: 'uint8' },
12
+ { name: 'safeTxGas', type: 'uint256' },
13
+ { name: 'baseGas', type: 'uint256' },
14
+ { name: 'gasPrice', type: 'uint256' },
15
+ { name: 'gasToken', type: 'address' },
16
+ { name: 'refundReceiver', type: 'address' },
17
+ { name: 'nonce', type: 'uint256' },
18
+ ],
19
+ };
20
+ const assertAddress = (value, label) => {
21
+ if (!isAddress(value)) {
22
+ throw new Error(`${label} must be a valid address, got "${value}".`);
23
+ }
24
+ return value;
25
+ };
26
+ const assertHex = (value, label) => {
27
+ if (!/^0x[0-9a-fA-F]*$/.test(value)) {
28
+ throw new Error(`${label} must be 0x-prefixed hex, got "${value}".`);
29
+ }
30
+ return value;
31
+ };
32
+ const decimal = (value, fallback = 0n) => {
33
+ if (value === undefined)
34
+ return fallback.toString();
35
+ if (typeof value === 'bigint')
36
+ return value.toString();
37
+ if (typeof value === 'number') {
38
+ if (!Number.isSafeInteger(value) || value < 0) {
39
+ throw new Error(`Safe numeric fields must be non-negative safe integers, got ${value}.`);
40
+ }
41
+ return String(value);
42
+ }
43
+ if (/^\d+$/.test(value))
44
+ return value;
45
+ if (/^0x[0-9a-fA-F]+$/.test(value))
46
+ return BigInt(value).toString();
47
+ throw new Error(`Safe numeric fields must be decimal strings, 0x hex, bigint, or number, got "${value}".`);
48
+ };
49
+ const bigintValue = (value) => BigInt(value);
50
+ export function normalizeSafeTransactionData(data) {
51
+ return {
52
+ to: assertAddress(data.to, 'Safe transaction to'),
53
+ value: decimal(data.value),
54
+ data: assertHex(data.data ?? '0x', 'Safe transaction data'),
55
+ operation: data.operation ?? 0,
56
+ safeTxGas: decimal(data.safeTxGas),
57
+ baseGas: decimal(data.baseGas),
58
+ gasPrice: decimal(data.gasPrice),
59
+ gasToken: assertAddress(data.gasToken ?? ZERO_ADDRESS, 'Safe transaction gasToken'),
60
+ refundReceiver: assertAddress(data.refundReceiver ?? ZERO_ADDRESS, 'Safe transaction refundReceiver'),
61
+ nonce: decimal(data.nonce),
62
+ };
63
+ }
64
+ export function buildSafeTransactionTypedData(safeAddress, chainId, data) {
65
+ const normalized = normalizeSafeTransactionData(data);
66
+ return {
67
+ domain: {
68
+ chainId: BigInt(chainId),
69
+ verifyingContract: assertAddress(safeAddress, 'Safe address'),
70
+ },
71
+ primaryType: 'SafeTx',
72
+ types: SAFE_TRANSACTION_TYPED_DATA_TYPES,
73
+ message: {
74
+ to: normalized.to,
75
+ value: bigintValue(normalized.value),
76
+ data: normalized.data,
77
+ operation: normalized.operation,
78
+ safeTxGas: bigintValue(normalized.safeTxGas),
79
+ baseGas: bigintValue(normalized.baseGas),
80
+ gasPrice: bigintValue(normalized.gasPrice),
81
+ gasToken: normalized.gasToken,
82
+ refundReceiver: normalized.refundReceiver,
83
+ nonce: bigintValue(normalized.nonce),
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Safe protocol transaction hash, matching Safe.sol getTransactionHash:
89
+ * EIP-712 domain { chainId, verifyingContract: safeAddress } and SafeTx.
90
+ */
91
+ export function hashSafeTransactionTypedData(safeAddress, chainId, data) {
92
+ return hashTypedData(buildSafeTransactionTypedData(safeAddress, chainId, data));
93
+ }
94
+ /**
95
+ * Deterministic local Safe transaction hash for tests and service fixtures.
96
+ * Real Safe deployments should pass the protocol-computed safeTxHash.
97
+ */
98
+ export function hashSafeTransactionData(safeAddress, chainId, data) {
99
+ const normalized = normalizeSafeTransactionData(data);
100
+ return keccak256(encodeAbiParameters([
101
+ { type: 'address' },
102
+ { type: 'uint256' },
103
+ { type: 'address' },
104
+ { type: 'uint256' },
105
+ { type: 'bytes' },
106
+ { type: 'uint8' },
107
+ { type: 'uint256' },
108
+ { type: 'uint256' },
109
+ { type: 'uint256' },
110
+ { type: 'address' },
111
+ { type: 'address' },
112
+ { type: 'uint256' },
113
+ ], [
114
+ safeAddress,
115
+ BigInt(chainId),
116
+ normalized.to,
117
+ bigintValue(normalized.value),
118
+ normalized.data,
119
+ normalized.operation,
120
+ bigintValue(normalized.safeTxGas),
121
+ bigintValue(normalized.baseGas),
122
+ bigintValue(normalized.gasPrice),
123
+ normalized.gasToken,
124
+ normalized.refundReceiver,
125
+ bigintValue(normalized.nonce),
126
+ ]));
127
+ }
128
+ export function deterministicSafeSignature(safeTxHash, owner) {
129
+ const r = keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'address' }, { type: 'string' }], [
130
+ safeTxHash,
131
+ owner,
132
+ 'r',
133
+ ]));
134
+ const s = keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'address' }, { type: 'string' }], [
135
+ safeTxHash,
136
+ owner,
137
+ 's',
138
+ ]));
139
+ return `${r}${s.slice(2)}1b`;
140
+ }
141
+ const normalizeConfirmation = (input) => ({
142
+ owner: assertAddress(String(input.owner ?? input.ownerAddress ?? ZERO_ADDRESS), 'Safe confirmation owner'),
143
+ signature: assertHex(String(input.signature ?? '0x'), 'Safe confirmation signature'),
144
+ ...(input.submissionDate ? { submissionDate: String(input.submissionDate) } : {}),
145
+ });
146
+ const normalizeServiceTransaction = (input, fallback) => {
147
+ const data = normalizeSafeTransactionData({
148
+ to: assertAddress(String(input.to), 'Safe transaction to'),
149
+ value: String(input.value ?? '0'),
150
+ data: assertHex(String(input.data ?? '0x'), 'Safe transaction data'),
151
+ operation: Number(input.operation ?? 0),
152
+ safeTxGas: String(input.safeTxGas ?? '0'),
153
+ baseGas: String(input.baseGas ?? '0'),
154
+ gasPrice: String(input.gasPrice ?? '0'),
155
+ gasToken: assertAddress(String(input.gasToken ?? ZERO_ADDRESS), 'Safe transaction gasToken'),
156
+ refundReceiver: assertAddress(String(input.refundReceiver ?? ZERO_ADDRESS), 'Safe transaction refundReceiver'),
157
+ nonce: String(input.nonce ?? '0'),
158
+ });
159
+ return {
160
+ ...data,
161
+ safeAddress: assertAddress(String(input.safe ?? input.safeAddress ?? fallback?.safeAddress ?? ZERO_ADDRESS), 'Safe transaction safeAddress'),
162
+ safeTxHash: assertHex(String(input.safeTxHash ?? input.contractTransactionHash), 'Safe transaction safeTxHash'),
163
+ senderAddress: assertAddress(String(input.sender ?? input.senderAddress ?? fallback?.senderAddress ?? ZERO_ADDRESS), 'Safe transaction senderAddress'),
164
+ ...(input.origin ? { origin: String(input.origin) } : {}),
165
+ confirmations: Array.isArray(input.confirmations)
166
+ ? input.confirmations.map((item) => normalizeConfirmation(item))
167
+ : [],
168
+ ...(input.confirmationsRequired !== undefined
169
+ ? { confirmationsRequired: Number(input.confirmationsRequired) }
170
+ : {}),
171
+ isExecuted: Boolean(input.isExecuted ?? input.executed),
172
+ ...(input.transactionHash ? { transactionHash: assertHex(String(input.transactionHash), 'Safe transaction hash') } : {}),
173
+ ...(input.executor ? { executorAddress: assertAddress(String(input.executor), 'Safe transaction executor') } : {}),
174
+ ...(input.executorAddress
175
+ ? { executorAddress: assertAddress(String(input.executorAddress), 'Safe transaction executor') }
176
+ : {}),
177
+ ...(input.submissionDate ? { submissionDate: String(input.submissionDate) } : {}),
178
+ };
179
+ };
180
+ const hasResponseBody = (body) => body !== undefined && Object.keys(body).length > 0;
181
+ export class SafeTransactionServiceClient {
182
+ fetchImpl;
183
+ baseUrl;
184
+ apiPrefix;
185
+ headers;
186
+ chainId;
187
+ safeTxHashStrategy;
188
+ constructor(options) {
189
+ if (!options.baseUrl) {
190
+ throw new Error('SafeTransactionServiceClient requires baseUrl.');
191
+ }
192
+ this.baseUrl = options.baseUrl.replace(/\/+$/, '');
193
+ this.apiPrefix = options.apiPrefix ? `/${options.apiPrefix.replace(/^\/+|\/+$/g, '')}` : '';
194
+ this.chainId = options.chainId;
195
+ this.safeTxHashStrategy =
196
+ options.safeTxHashStrategy ?? (options.chainId === undefined ? 'fixture' : 'eip712');
197
+ this.fetchImpl = options.fetch ?? fetch;
198
+ this.headers = options.headers ?? {};
199
+ }
200
+ async proposeTransaction(proposal) {
201
+ const safeAddress = assertAddress(proposal.safeAddress, 'Safe address');
202
+ const senderAddress = assertAddress(proposal.senderAddress, 'Safe senderAddress');
203
+ const data = normalizeSafeTransactionData(proposal.data);
204
+ const safeTxHash = proposal.safeTxHash ?? this.hashSafeTransaction(safeAddress, proposal.data);
205
+ const body = await this.request(`/safes/${safeAddress}/multisig-transactions/`, {
206
+ method: 'POST',
207
+ body: {
208
+ ...data,
209
+ contractTransactionHash: safeTxHash,
210
+ safeTxHash,
211
+ sender: senderAddress,
212
+ senderAddress,
213
+ signature: proposal.senderSignature,
214
+ origin: proposal.origin,
215
+ },
216
+ });
217
+ if (hasResponseBody(body)) {
218
+ return normalizeServiceTransaction(body, { safeAddress, senderAddress });
219
+ }
220
+ return this.getTransaction(safeTxHash);
221
+ }
222
+ async confirmTransaction(safeTxHash, confirmation) {
223
+ await this.request(`/multisig-transactions/${safeTxHash}/confirmations/`, {
224
+ method: 'POST',
225
+ body: { signature: confirmation.signature },
226
+ });
227
+ return this.getTransaction(safeTxHash);
228
+ }
229
+ async getTransaction(safeTxHash) {
230
+ const body = await this.request(`/multisig-transactions/${safeTxHash}/`, { method: 'GET' });
231
+ if (!hasResponseBody(body)) {
232
+ throw new Error(`Safe Transaction Service returned an empty transaction response for ${safeTxHash}.`);
233
+ }
234
+ return normalizeServiceTransaction(body);
235
+ }
236
+ async listTransactions(safeAddress) {
237
+ const body = await this.request(`/safes/${safeAddress}/multisig-transactions/`, { method: 'GET' });
238
+ return (body?.results ?? []).map((item) => normalizeServiceTransaction(item, { safeAddress }));
239
+ }
240
+ async listConfirmations(safeTxHash) {
241
+ const body = await this.request(`/multisig-transactions/${safeTxHash}/confirmations/`, { method: 'GET' });
242
+ return (body?.results ?? []).map((item) => normalizeConfirmation(item));
243
+ }
244
+ async request(path, options) {
245
+ const response = await this.fetchImpl(`${this.baseUrl}${this.apiPrefix}${path}`, {
246
+ method: options.method,
247
+ headers: {
248
+ accept: 'application/json',
249
+ ...(options.body ? { 'content-type': 'application/json' } : {}),
250
+ ...this.headers,
251
+ },
252
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
253
+ });
254
+ const text = await response.text();
255
+ if (!response.ok) {
256
+ throw new Error(`Safe Transaction Service ${options.method} ${path} failed with HTTP ${response.status}: ${text.slice(0, 500)}`);
257
+ }
258
+ const trimmed = text.trim();
259
+ return trimmed ? JSON.parse(trimmed) : undefined;
260
+ }
261
+ hashSafeTransaction(safeAddress, data) {
262
+ if (this.safeTxHashStrategy === 'eip712') {
263
+ if (this.chainId === undefined) {
264
+ throw new Error('SafeTransactionServiceClient requires chainId to compute EIP-712 safeTxHash. Pass safeTxHash explicitly or configure chainId.');
265
+ }
266
+ return hashSafeTransactionTypedData(safeAddress, this.chainId, data);
267
+ }
268
+ return hashSafeTransactionData(safeAddress, this.chainId ?? 0n, data);
269
+ }
270
+ }
271
+ export class InMemorySafeTransactionService {
272
+ transactions = new Map();
273
+ async proposeTransaction(proposal) {
274
+ const safeAddress = assertAddress(proposal.safeAddress, 'Safe address');
275
+ const senderAddress = assertAddress(proposal.senderAddress, 'Safe senderAddress');
276
+ const safeTxHash = proposal.safeTxHash ?? hashSafeTransactionData(safeAddress, 0n, proposal.data);
277
+ const existing = this.transactions.get(safeTxHash);
278
+ if (existing) {
279
+ throw new Error(`Safe transaction "${safeTxHash}" already exists.`);
280
+ }
281
+ const record = {
282
+ ...normalizeSafeTransactionData(proposal.data),
283
+ safeAddress,
284
+ safeTxHash,
285
+ senderAddress,
286
+ ...(proposal.origin ? { origin: proposal.origin } : {}),
287
+ confirmations: [
288
+ {
289
+ owner: senderAddress,
290
+ signature: proposal.senderSignature,
291
+ submissionDate: new Date().toISOString(),
292
+ },
293
+ ],
294
+ ...(proposal.confirmationsRequired !== undefined
295
+ ? { confirmationsRequired: proposal.confirmationsRequired }
296
+ : {}),
297
+ isExecuted: false,
298
+ submissionDate: new Date().toISOString(),
299
+ };
300
+ this.transactions.set(safeTxHash, record);
301
+ return cloneTransaction(record);
302
+ }
303
+ async confirmTransaction(safeTxHash, confirmation) {
304
+ const record = this.mustGet(safeTxHash);
305
+ if (record.confirmations.some((item) => sameAddress(item.owner, confirmation.owner))) {
306
+ return cloneTransaction(record);
307
+ }
308
+ record.confirmations.push({
309
+ ...confirmation,
310
+ submissionDate: confirmation.submissionDate ?? new Date().toISOString(),
311
+ });
312
+ return cloneTransaction(record);
313
+ }
314
+ async getTransaction(safeTxHash) {
315
+ return cloneTransaction(this.mustGet(safeTxHash));
316
+ }
317
+ async listTransactions(safeAddress) {
318
+ return [...this.transactions.values()]
319
+ .filter((transaction) => sameAddress(transaction.safeAddress, safeAddress))
320
+ .map(cloneTransaction);
321
+ }
322
+ async listConfirmations(safeTxHash) {
323
+ return cloneTransaction(this.mustGet(safeTxHash)).confirmations;
324
+ }
325
+ async markExecuted(safeTxHash, execution) {
326
+ const record = this.mustGet(safeTxHash);
327
+ record.isExecuted = true;
328
+ record.transactionHash = execution.transactionHash;
329
+ record.executorAddress = execution.executorAddress;
330
+ return cloneTransaction(record);
331
+ }
332
+ mustGet(safeTxHash) {
333
+ const record = this.transactions.get(safeTxHash);
334
+ if (!record) {
335
+ throw new Error(`Unknown Safe transaction "${safeTxHash}".`);
336
+ }
337
+ return record;
338
+ }
339
+ }
340
+ export class SafeWalletHarness {
341
+ safeAddress;
342
+ owners;
343
+ threshold;
344
+ chainId;
345
+ transactionService;
346
+ rpcClient;
347
+ safeTxHashStrategy;
348
+ nonce = 0n;
349
+ constructor(options) {
350
+ this.safeAddress = assertAddress(options.safeAddress, 'Safe address');
351
+ this.owners = options.owners.map((owner) => assertAddress(owner, 'Safe owner'));
352
+ this.threshold = options.threshold;
353
+ this.chainId = options.chainId;
354
+ this.transactionService = options.transactionService;
355
+ this.rpcClient = options.rpcClient;
356
+ this.safeTxHashStrategy = options.safeTxHashStrategy ?? 'eip712';
357
+ if (this.owners.length === 0) {
358
+ throw new Error('SafeWalletHarness requires at least one owner.');
359
+ }
360
+ if (!Number.isInteger(this.threshold) || this.threshold < 1 || this.threshold > this.owners.length) {
361
+ throw new Error(`Safe threshold must be between 1 and owner count (${this.owners.length}), got ${this.threshold}.`);
362
+ }
363
+ }
364
+ async proposeTransaction(options) {
365
+ const proposer = options.proposer ?? this.owners[0];
366
+ this.assertOwner(proposer);
367
+ let allocatedNonce;
368
+ const data = {
369
+ ...options.transaction,
370
+ nonce: options.transaction.nonce ?? (allocatedNonce = this.nextNonce()),
371
+ };
372
+ const safeTxHash = this.hashSafeTransaction(data);
373
+ const signature = options.signature ?? deterministicSafeSignature(safeTxHash, proposer);
374
+ try {
375
+ return await this.transactionService.proposeTransaction({
376
+ safeAddress: this.safeAddress,
377
+ senderAddress: proposer,
378
+ safeTxHash,
379
+ senderSignature: signature,
380
+ confirmationsRequired: this.threshold,
381
+ origin: options.origin,
382
+ data,
383
+ });
384
+ }
385
+ catch (error) {
386
+ if (allocatedNonce !== undefined)
387
+ this.rollbackNonce(allocatedNonce);
388
+ throw error;
389
+ }
390
+ }
391
+ async confirmTransaction(safeTxHash, options) {
392
+ this.assertOwner(options.owner);
393
+ return this.transactionService.confirmTransaction(safeTxHash, {
394
+ owner: options.owner,
395
+ signature: options.signature ?? deterministicSafeSignature(safeTxHash, options.owner),
396
+ });
397
+ }
398
+ async executeTransaction(safeTxHash, options = {}) {
399
+ const transaction = await this.transactionService.getTransaction(safeTxHash);
400
+ if (transaction.isExecuted) {
401
+ return { txHash: transaction.transactionHash, transaction };
402
+ }
403
+ if (transaction.confirmations.length < this.threshold) {
404
+ throw new Error(`Safe transaction "${safeTxHash}" has ${transaction.confirmations.length}/${this.threshold} confirmations.`);
405
+ }
406
+ const executor = options.executor ?? transaction.confirmations[0]?.owner ?? this.owners[0];
407
+ this.assertOwner(executor);
408
+ let txHash;
409
+ if (this.rpcClient) {
410
+ txHash = (await this.rpcClient.request({
411
+ method: 'eth_sendTransaction',
412
+ params: [
413
+ {
414
+ from: executor,
415
+ to: transaction.to,
416
+ value: toHex(bigintValue(transaction.value)),
417
+ data: transaction.data,
418
+ },
419
+ ],
420
+ }));
421
+ }
422
+ const executed = txHash && this.transactionService.markExecuted
423
+ ? await this.transactionService.markExecuted(safeTxHash, {
424
+ transactionHash: txHash,
425
+ executorAddress: executor,
426
+ })
427
+ : transaction;
428
+ return { txHash, transaction: executed };
429
+ }
430
+ async getTransaction(safeTxHash) {
431
+ return this.transactionService.getTransaction(safeTxHash);
432
+ }
433
+ async listTransactions() {
434
+ return this.transactionService.listTransactions(this.safeAddress);
435
+ }
436
+ async listConfirmations(safeTxHash) {
437
+ return this.transactionService.listConfirmations(safeTxHash);
438
+ }
439
+ get currentNonce() {
440
+ return this.nonce;
441
+ }
442
+ async requestRpc(request) {
443
+ if (!this.rpcClient) {
444
+ throw new Error('SafeWalletHarness has no rpcClient configured for Safe App rpcCall requests.');
445
+ }
446
+ return this.rpcClient.request(request);
447
+ }
448
+ nextNonce() {
449
+ const value = this.nonce;
450
+ this.nonce += 1n;
451
+ return value.toString();
452
+ }
453
+ rollbackNonce(value) {
454
+ const nonce = BigInt(value);
455
+ if (this.nonce === nonce + 1n) {
456
+ this.nonce = nonce;
457
+ }
458
+ }
459
+ hashSafeTransaction(data) {
460
+ if (this.safeTxHashStrategy === 'fixture') {
461
+ return hashSafeTransactionData(this.safeAddress, this.chainId, data);
462
+ }
463
+ return hashSafeTransactionTypedData(this.safeAddress, this.chainId, data);
464
+ }
465
+ assertOwner(owner) {
466
+ if (!this.owners.some((candidate) => sameAddress(candidate, owner))) {
467
+ throw new Error(`"${owner}" is not a Safe owner.`);
468
+ }
469
+ }
470
+ }
471
+ export async function handleSafeAppRequest(safe, request, options = {}) {
472
+ if (options.allowedOrigins) {
473
+ const origin = options.origin === undefined ? 'null' : toOrigin(options.origin);
474
+ if (!options.allowedOrigins.map(toOrigin).includes(origin)) {
475
+ throw new Error(`Safe App origin "${origin}" is not allowed.`);
476
+ }
477
+ }
478
+ switch (request.method) {
479
+ case 'getSafeInfo':
480
+ return {
481
+ safeAddress: safe.safeAddress,
482
+ chainId: Number(safe.chainId),
483
+ threshold: safe.threshold,
484
+ owners: [...safe.owners],
485
+ isReadOnly: options.safeInfo?.isReadOnly ?? false,
486
+ nonce: options.safeInfo?.nonce ?? Number(safe.currentNonce),
487
+ implementation: options.safeInfo?.implementation ?? ZERO_ADDRESS,
488
+ modules: options.safeInfo?.modules === undefined
489
+ ? null
490
+ : options.safeInfo.modules === null
491
+ ? null
492
+ : [...options.safeInfo.modules],
493
+ fallbackHandler: options.safeInfo?.fallbackHandler ?? null,
494
+ guard: options.safeInfo?.guard ?? null,
495
+ version: options.safeInfo?.version ?? null,
496
+ };
497
+ case 'getChainInfo':
498
+ const blockExplorerUriTemplate = options.chainInfo?.blockExplorerUriTemplate;
499
+ return {
500
+ chainName: options.chainInfo?.chainName ?? `Chain ${String(safe.chainId)}`,
501
+ chainId: String(safe.chainId),
502
+ shortName: options.chainInfo?.shortName ?? String(safe.chainId),
503
+ nativeCurrency: options.chainInfo?.nativeCurrency ?? {
504
+ name: 'Ether',
505
+ symbol: 'ETH',
506
+ decimals: 18,
507
+ logoUri: '',
508
+ },
509
+ blockExplorerUriTemplate: {
510
+ address: blockExplorerUriTemplate?.address ?? '',
511
+ txHash: blockExplorerUriTemplate?.txHash ?? blockExplorerUriTemplate?.tx ?? '',
512
+ api: blockExplorerUriTemplate?.api ?? '',
513
+ },
514
+ };
515
+ case 'getEnvironmentInfo':
516
+ return { origin: options.environmentOrigin ?? options.origin ?? 'null' };
517
+ case 'sendTransactions': {
518
+ const params = asRecord(request.params);
519
+ const txs = params.txs;
520
+ if (!Array.isArray(txs) || txs.length === 0) {
521
+ throw new Error('Safe Apps sendTransactions requires a non-empty txs array.');
522
+ }
523
+ const normalizedTxs = txs.map(normalizeSafeAppTransaction);
524
+ const safeTxGas = asRecord(params.params).safeTxGas;
525
+ const transaction = normalizedTxs.length === 1
526
+ ? {
527
+ to: normalizedTxs[0].to,
528
+ value: normalizedTxs[0].value,
529
+ data: normalizedTxs[0].data,
530
+ safeTxGas,
531
+ }
532
+ : {
533
+ to: assertAddress(options.multiSendAddress ?? SAFE_MULTISEND_CALL_ONLY_ADDRESS, 'Safe Apps multiSendAddress'),
534
+ value: '0',
535
+ data: encodeSafeMultiSendCall(normalizedTxs),
536
+ operation: 1,
537
+ safeTxGas,
538
+ };
539
+ const proposed = await safe.proposeTransaction({
540
+ proposer: options.proposer,
541
+ origin: typeof params.origin === 'string' ? params.origin : options.environmentOrigin,
542
+ transaction,
543
+ });
544
+ return { safeTxHash: proposed.safeTxHash };
545
+ }
546
+ case 'getTxBySafeTxHash': {
547
+ const params = asRecord(request.params);
548
+ const safeTxHash = assertHex(String(params.safeTxHash), 'safeTxHash');
549
+ const transaction = await safe.getTransaction(safeTxHash);
550
+ return safeTransactionToGatewayDetails(transaction);
551
+ }
552
+ case 'rpcCall': {
553
+ const params = asRecord(request.params);
554
+ const method = String(params.call);
555
+ if (method === 'safe_setSettings') {
556
+ return Array.isArray(params.params) ? params.params[0] : params.params;
557
+ }
558
+ return safe.requestRpc({
559
+ method,
560
+ params: Array.isArray(params.params) ? params.params : [],
561
+ });
562
+ }
563
+ case 'signMessage': {
564
+ const params = asRecord(request.params);
565
+ const message = String(params.message ?? '');
566
+ const messageHash = keccak256(encodeAbiParameters([{ type: 'string' }], [message]));
567
+ return {
568
+ messageHash,
569
+ signature: deterministicSafeSignature(messageHash, safe.owners[0]),
570
+ };
571
+ }
572
+ case 'signTypedMessage': {
573
+ const params = asRecord(request.params);
574
+ const messageHash = keccak256(encodeAbiParameters([{ type: 'string' }], [JSON.stringify(params.typedData ?? {})]));
575
+ return {
576
+ messageHash,
577
+ signature: deterministicSafeSignature(messageHash, safe.owners[0]),
578
+ };
579
+ }
580
+ case 'getOffChainSignature': {
581
+ const safeTxHash = assertHex(String(request.params ?? '0x'), 'message hash');
582
+ return deterministicSafeSignature(safeTxHash, safe.owners[0]);
583
+ }
584
+ case 'wallet_getPermissions':
585
+ return [...(options.permissions ?? [])];
586
+ case 'wallet_requestPermissions':
587
+ return [...(options.permissions ?? [])];
588
+ case 'requestAddressBook':
589
+ return [...(options.addressBook ?? [])];
590
+ case 'getSafeBalances':
591
+ return normalizeSafeAppBalances(options.balances);
592
+ default:
593
+ throw new Error(`Safe Apps SDK method "${request.method}" is not implemented.`);
594
+ }
595
+ }
596
+ export async function injectSafeAppBridge(page, safe, options = {}) {
597
+ const context = page.context();
598
+ try {
599
+ await context.exposeBinding(SAFE_APP_BRIDGE_BINDING, async (_source, payload) => {
600
+ return handleSafeAppRequest(safe, payload.request, {
601
+ ...options,
602
+ origin: payload.origin,
603
+ });
604
+ });
605
+ }
606
+ catch (error) {
607
+ if (error instanceof Error && /already registered/i.test(error.message)) {
608
+ throw new Error('A Safe App bridge is already injected into this browser context. Create one bridge per context.', { cause: error });
609
+ }
610
+ throw error;
611
+ }
612
+ const script = buildSafeAppBridgeScript(options.version ?? '1.0.0');
613
+ await context.addInitScript(script);
614
+ await Promise.all(context.pages().map((target) => target.evaluate(script).catch(() => undefined)));
615
+ }
616
+ export function buildSafeAppBridgeScript(version = '1.0.0') {
617
+ return `
618
+ (() => {
619
+ if (window.__web3TesterSafeAppBridgeInstalled) return;
620
+ Object.defineProperty(window, '__web3TesterSafeAppBridgeInstalled', {
621
+ value: true,
622
+ configurable: true,
623
+ });
624
+
625
+ const isSafeAppRequest = (data) =>
626
+ data &&
627
+ typeof data.id === 'string' &&
628
+ typeof data.method === 'string' &&
629
+ data.env &&
630
+ typeof data.env.sdkVersion === 'string';
631
+
632
+ window.addEventListener('message', async (event) => {
633
+ if (!isSafeAppRequest(event.data)) return;
634
+ const target = event.source;
635
+ if (!target || typeof target.postMessage !== 'function') return;
636
+ const targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
637
+ try {
638
+ const data = await window.${SAFE_APP_BRIDGE_BINDING}({
639
+ request: event.data,
640
+ origin: event.origin,
641
+ });
642
+ target.postMessage({ id: event.data.id, success: true, version: ${JSON.stringify(version)}, data }, targetOrigin);
643
+ } catch (error) {
644
+ target.postMessage({
645
+ id: event.data.id,
646
+ success: false,
647
+ version: ${JSON.stringify(version)},
648
+ error: error instanceof Error ? error.message : String(error),
649
+ }, targetOrigin);
650
+ }
651
+ });
652
+ })();
653
+ `;
654
+ }
655
+ const sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
656
+ const cloneTransaction = (transaction) => ({
657
+ ...transaction,
658
+ confirmations: transaction.confirmations.map((confirmation) => ({ ...confirmation })),
659
+ });
660
+ const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
661
+ ? value
662
+ : {};
663
+ const normalizeSafeAppTransaction = (value) => {
664
+ const tx = asRecord(value);
665
+ return {
666
+ to: assertAddress(String(tx.to ?? ZERO_ADDRESS), 'Safe App transaction to'),
667
+ value: decimal(tx.value),
668
+ data: assertHex(String(tx.data ?? '0x'), 'Safe App transaction data'),
669
+ };
670
+ };
671
+ const hexByteLength = (hex) => BigInt((hex.length - 2) / 2);
672
+ const encodeSafeMultiSendTransactions = (transactions) => {
673
+ const packed = transactions
674
+ .map((transaction) => [
675
+ toHex(0, { size: 1 }).slice(2),
676
+ transaction.to.slice(2).toLowerCase(),
677
+ toHex(BigInt(transaction.value), { size: 32 }).slice(2),
678
+ toHex(hexByteLength(transaction.data), { size: 32 }).slice(2),
679
+ transaction.data.slice(2),
680
+ ].join(''))
681
+ .join('');
682
+ return `0x${packed}`;
683
+ };
684
+ const encodeSafeMultiSendCall = (transactions) => {
685
+ const encodedTransactions = encodeSafeMultiSendTransactions(transactions);
686
+ const encodedArgument = encodeAbiParameters([{ type: 'bytes' }], [encodedTransactions]);
687
+ return `${SAFE_MULTISEND_SELECTOR}${encodedArgument.slice(2)}`;
688
+ };
689
+ const normalizeSafeAppBalances = (balances) => {
690
+ if (isSafeAppBalances(balances)) {
691
+ return {
692
+ fiatTotal: String(balances.fiatTotal),
693
+ items: balances.items.map((item) => ({ ...item, tokenInfo: { ...item.tokenInfo } })),
694
+ };
695
+ }
696
+ return {
697
+ fiatTotal: '0',
698
+ items: (balances ?? []).map((item) => ({ ...item, tokenInfo: { ...item.tokenInfo } })),
699
+ };
700
+ };
701
+ const isSafeAppBalances = (balances) => Boolean(balances &&
702
+ !Array.isArray(balances) &&
703
+ typeof balances === 'object' &&
704
+ 'fiatTotal' in balances &&
705
+ 'items' in balances);
706
+ const safeTransactionToGatewayDetails = (transaction) => ({
707
+ safeAddress: transaction.safeAddress,
708
+ safeTxHash: transaction.safeTxHash,
709
+ txHash: transaction.transactionHash,
710
+ txStatus: transaction.isExecuted ? 'SUCCESS' : 'AWAITING_CONFIRMATIONS',
711
+ txInfo: {
712
+ type: 'Custom',
713
+ to: { value: transaction.to },
714
+ value: transaction.value,
715
+ dataSize: transaction.data === '0x' ? '0' : String((transaction.data.length - 2) / 2),
716
+ },
717
+ txData: {
718
+ hexData: transaction.data,
719
+ to: { value: transaction.to },
720
+ value: transaction.value,
721
+ operation: transaction.operation,
722
+ },
723
+ detailedExecutionInfo: {
724
+ type: 'MULTISIG',
725
+ nonce: Number(BigInt(transaction.nonce)),
726
+ confirmationsRequired: transaction.confirmationsRequired ?? transaction.confirmations.length,
727
+ confirmationsSubmitted: transaction.confirmations.length,
728
+ confirmations: transaction.confirmations.map((confirmation) => ({
729
+ signer: { value: confirmation.owner },
730
+ signature: confirmation.signature,
731
+ submittedAt: confirmation.submissionDate,
732
+ })),
733
+ },
734
+ });
735
+ const toOrigin = (urlOrOrigin) => {
736
+ try {
737
+ return new URL(urlOrOrigin).origin;
738
+ }
739
+ catch {
740
+ return urlOrOrigin;
741
+ }
742
+ };
743
+ //# sourceMappingURL=safe.js.map