ox 1.6.2 → 1.7.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/core/AesGcm.d.ts +67 -2
  3. package/dist/core/AesGcm.d.ts.map +1 -1
  4. package/dist/core/AesGcm.js +79 -10
  5. package/dist/core/AesGcm.js.map +1 -1
  6. package/dist/core/Ed25519.d.ts +83 -2
  7. package/dist/core/Ed25519.d.ts.map +1 -1
  8. package/dist/core/Ed25519.js +79 -4
  9. package/dist/core/Ed25519.js.map +1 -1
  10. package/dist/core/MlDsa44.d.ts +84 -5
  11. package/dist/core/MlDsa44.d.ts.map +1 -1
  12. package/dist/core/MlDsa44.js +79 -4
  13. package/dist/core/MlDsa44.js.map +1 -1
  14. package/dist/core/P256.d.ts +82 -1
  15. package/dist/core/P256.d.ts.map +1 -1
  16. package/dist/core/P256.js +78 -0
  17. package/dist/core/P256.js.map +1 -1
  18. package/dist/core/Secp256k1.d.ts +85 -1
  19. package/dist/core/Secp256k1.d.ts.map +1 -1
  20. package/dist/core/Secp256k1.js +74 -3
  21. package/dist/core/Secp256k1.js.map +1 -1
  22. package/dist/core/internal/keyDerivation.d.ts +13 -0
  23. package/dist/core/internal/keyDerivation.d.ts.map +1 -0
  24. package/dist/core/internal/keyDerivation.js +13 -0
  25. package/dist/core/internal/keyDerivation.js.map +1 -0
  26. package/dist/tempo/KeyAuthorization.d.ts +16 -12
  27. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  28. package/dist/tempo/KeyAuthorization.js +7 -13
  29. package/dist/tempo/KeyAuthorization.js.map +1 -1
  30. package/dist/tempo/MultisigConfig.d.ts +20 -17
  31. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  32. package/dist/tempo/MultisigConfig.js +17 -16
  33. package/dist/tempo/MultisigConfig.js.map +1 -1
  34. package/dist/tempo/SignatureEnvelope.d.ts +19 -17
  35. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  36. package/dist/tempo/SignatureEnvelope.js +35 -27
  37. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  38. package/dist/tempo/index.d.ts +2 -2
  39. package/dist/tempo/index.js +2 -2
  40. package/dist/zod/tempo/KeyAuthorization.d.ts +231 -22
  41. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  42. package/dist/zod/tempo/KeyAuthorization.js +10 -2
  43. package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
  44. package/dist/zod/tempo/RpcSchemaTempo.d.ts +82 -8
  45. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  46. package/dist/zod/tempo/Transaction.d.ts +246 -24
  47. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  48. package/dist/zod/tempo/TransactionRequest.d.ts +149 -14
  49. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  50. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +78 -6
  51. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/core/AesGcm.ts +137 -30
  54. package/src/core/Ed25519.ts +135 -4
  55. package/src/core/MlDsa44.ts +135 -4
  56. package/src/core/P256.ts +135 -1
  57. package/src/core/Secp256k1.ts +134 -3
  58. package/src/core/_test/AesGcm.test-d.ts +19 -0
  59. package/src/core/_test/AesGcm.test.ts +90 -1
  60. package/src/core/_test/Ed25519.test-d.ts +24 -0
  61. package/src/core/_test/Ed25519.test.ts +87 -1
  62. package/src/core/_test/MlDsa44.test-d.ts +24 -0
  63. package/src/core/_test/MlDsa44.test.ts +69 -1
  64. package/src/core/_test/P256.test-d.ts +26 -0
  65. package/src/core/_test/P256.test.ts +98 -1
  66. package/src/core/_test/Secp256k1.test-d.ts +27 -0
  67. package/src/core/_test/Secp256k1.test.ts +112 -1
  68. package/src/core/internal/keyDerivation.ts +34 -0
  69. package/src/tempo/KeyAuthorization.test-d.ts +41 -6
  70. package/src/tempo/KeyAuthorization.test.ts +77 -18
  71. package/src/tempo/KeyAuthorization.ts +36 -36
  72. package/src/tempo/MultisigConfig.test.ts +28 -8
  73. package/src/tempo/MultisigConfig.ts +28 -19
  74. package/src/tempo/SignatureEnvelope.test.ts +64 -19
  75. package/src/tempo/SignatureEnvelope.ts +45 -35
  76. package/src/tempo/index.ts +2 -2
  77. package/src/tempo/multisig.e2e.test.ts +402 -145
  78. package/src/version.ts +1 -1
  79. package/src/zod/tempo/KeyAuthorization.ts +12 -2
  80. package/src/zod/tempo/_test/KeyAuthorization.test.ts +19 -5
@@ -8,38 +8,58 @@ import * as TransactionReceipt from './TransactionReceipt.js'
8
8
  import * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
9
9
 
10
10
  const chainId = chain.id
11
+ const updateConfig = AbiFunction.from(
12
+ 'function updateConfig(uint8 threshold, (address owner, uint8 weight)[] owners)',
13
+ )
11
14
 
12
15
  describe('behavior: multisig (TIP-1061)', () => {
13
- function setup(parameters: { count: number; threshold: number }) {
14
- const { count, threshold } = parameters
16
+ function createKey() {
17
+ const privateKey = Secp256k1.randomPrivateKey()
18
+ const address = Address.fromPublicKey(
19
+ Secp256k1.getPublicKey({ privateKey }),
20
+ )
21
+ return { address, privateKey } as const
22
+ }
23
+
24
+ function setup(parameters: {
25
+ count: number
26
+ threshold: number
27
+ weights?: readonly number[] | undefined
28
+ }) {
29
+ const { count, threshold, weights = Array(count).fill(1) } = parameters
15
30
  const ownerKeys = Array.from({ length: count }, () => {
16
31
  const privateKey = Secp256k1.randomPrivateKey()
17
32
  const address = Address.fromPublicKey(
18
33
  Secp256k1.getPublicKey({ privateKey }),
19
34
  )
20
35
  return { address, privateKey } as const
21
- })
36
+ }).sort((a, b) => a.address.localeCompare(b.address))
22
37
 
23
- const genesisConfig = MultisigConfig.from({
38
+ const initialConfig = MultisigConfig.from({
24
39
  salt: Hex.random(32),
25
40
  threshold,
26
- owners: ownerKeys.map((key) => ({
41
+ owners: ownerKeys.map((key, index) => ({
27
42
  owner: key.address,
28
- weight: 1,
43
+ weight: weights[index]!,
29
44
  })),
30
45
  })
31
- const account = MultisigConfig.getAddress(genesisConfig)
46
+ const account = MultisigConfig.getAddress(initialConfig)
32
47
 
33
- return { account, genesisConfig, ownerKeys } as const
48
+ return { account, initialConfig, ownerKeys } as const
34
49
  }
35
50
 
36
51
  function approve(parameters: {
37
- genesisConfig: MultisigConfig.Config
52
+ initialConfig: MultisigConfig.Config
38
53
  payload: Hex.Hex
39
54
  signers: readonly { privateKey: Hex.Hex }[]
55
+ version?: bigint | undefined
40
56
  }) {
41
- const { genesisConfig, payload, signers } = parameters
42
- const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
57
+ const { initialConfig, payload, signers, version = 0n } = parameters
58
+ const digest = MultisigConfig.getSignPayload({
59
+ payload,
60
+ initialConfig,
61
+ version,
62
+ })
43
63
  const signatures = signers.map((signer) =>
44
64
  SignatureEnvelope.from(
45
65
  Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
@@ -47,17 +67,92 @@ describe('behavior: multisig (TIP-1061)', () => {
47
67
  )
48
68
  // The node requires approvals ordered by recovered owner address.
49
69
  return SignatureEnvelope.sortMultisigApprovals({
50
- genesisConfig,
70
+ initialConfig,
51
71
  payload,
52
72
  signatures,
73
+ version,
74
+ })
75
+ }
76
+
77
+ function multisigSignature(parameters: {
78
+ initialConfig: MultisigConfig.Config
79
+ payload: Hex.Hex
80
+ signers: readonly { privateKey: Hex.Hex }[]
81
+ init?: boolean | undefined
82
+ version?: bigint | undefined
83
+ }) {
84
+ const { init = false, ...approval } = parameters
85
+ return SignatureEnvelope.from({
86
+ initialConfig: parameters.initialConfig,
87
+ ...(init ? { init: true } : {}),
88
+ signatures: approve(approval),
89
+ })
90
+ }
91
+
92
+ async function send(serialized: Hex.Hex) {
93
+ return (await client
94
+ .request({
95
+ method: 'eth_sendRawTransactionSync',
96
+ params: [serialized],
97
+ })
98
+ .then((transaction) => TransactionReceipt.fromRpc(transaction as any)))!
99
+ }
100
+
101
+ async function bootstrap(multisig: ReturnType<typeof setup>) {
102
+ await fundAddress(client, { address: multisig.account })
103
+ const transaction = TxEnvelopeTempo.from({
104
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
105
+ chainId,
106
+ feeToken: '0x20c0000000000000000000000000000000000001',
107
+ nonce: 0n,
108
+ gas: 5_000_000n,
109
+ maxFeePerGas: Value.fromGwei('20'),
110
+ maxPriorityFeePerGas: Value.fromGwei('10'),
111
+ })
112
+ const receipt = await send(
113
+ TxEnvelopeTempo.serialize(transaction, {
114
+ signature: multisigSignature({
115
+ initialConfig: multisig.initialConfig,
116
+ init: true,
117
+ payload: TxEnvelopeTempo.getSignPayload(transaction),
118
+ signers: multisig.ownerKeys,
119
+ }),
120
+ }),
121
+ )
122
+ expect(receipt.status).toBe('success')
123
+ }
124
+
125
+ function accessKeySignature(parameters: {
126
+ accessKey: { privateKey: Hex.Hex }
127
+ account: Address.Address
128
+ transaction: TxEnvelopeTempo.TxEnvelopeTempo
129
+ }) {
130
+ const { accessKey, account, transaction } = parameters
131
+ return SignatureEnvelope.from({
132
+ inner: SignatureEnvelope.from(
133
+ Secp256k1.sign({
134
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
135
+ from: account,
136
+ }),
137
+ privateKey: accessKey.privateKey,
138
+ }),
139
+ ),
140
+ type: 'keychain',
141
+ userAddress: account,
53
142
  })
54
143
  }
55
144
 
56
- test('behavior: bootstrap + spend (2-of-3 secp256k1)', async () => {
57
- const { account, genesisConfig, ownerKeys } = setup({
145
+ test('examples: bootstrap, initialized, and configuration rotation', async () => {
146
+ const { account, initialConfig, ownerKeys } = setup({
58
147
  count: 3,
59
148
  threshold: 2,
60
149
  })
150
+ const replacement = createKey()
151
+ const rotatedConfig = MultisigConfig.from({
152
+ salt: initialConfig.salt,
153
+ threshold: 1,
154
+ owners: [{ owner: replacement.address, weight: 1 }],
155
+ })
61
156
 
62
157
  await fundAddress(client, { address: account })
63
158
 
@@ -73,10 +168,10 @@ describe('behavior: multisig (TIP-1061)', () => {
73
168
 
74
169
  const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
75
170
  signature: SignatureEnvelope.from({
76
- genesisConfig,
171
+ initialConfig,
77
172
  init: true,
78
173
  signatures: approve({
79
- genesisConfig,
174
+ initialConfig,
80
175
  payload: TxEnvelopeTempo.getSignPayload(bootstrap),
81
176
  signers: [ownerKeys[0]!, ownerKeys[1]!],
82
177
  }),
@@ -105,9 +200,48 @@ describe('behavior: multisig (TIP-1061)', () => {
105
200
  expect(response.signature?.type).toBe('multisig')
106
201
  expect(
107
202
  (response.signature as SignatureEnvelope.Multisig | undefined)?.init,
108
- ).toEqual(genesisConfig)
203
+ ).toEqual(initialConfig)
109
204
  }
110
205
 
206
+ const updateNonce = await getTransactionCount(client, {
207
+ address: account,
208
+ blockTag: 'pending',
209
+ })
210
+ const update = TxEnvelopeTempo.from({
211
+ calls: [
212
+ {
213
+ to: '0xaacc000000000000000000000000000000000000',
214
+ data: AbiFunction.encodeData(updateConfig, [
215
+ rotatedConfig.threshold,
216
+ rotatedConfig.owners,
217
+ ]),
218
+ },
219
+ ],
220
+ chainId,
221
+ feeToken: '0x20c0000000000000000000000000000000000001',
222
+ nonce: BigInt(updateNonce),
223
+ gas: 5_000_000n,
224
+ maxFeePerGas: Value.fromGwei('20'),
225
+ maxPriorityFeePerGas: Value.fromGwei('10'),
226
+ })
227
+ const updateSigned = TxEnvelopeTempo.serialize(update, {
228
+ signature: SignatureEnvelope.from({
229
+ initialConfig,
230
+ signatures: approve({
231
+ initialConfig,
232
+ payload: TxEnvelopeTempo.getSignPayload(update),
233
+ signers: [ownerKeys[0]!, ownerKeys[1]!],
234
+ }),
235
+ }),
236
+ })
237
+ const updateReceipt = (await client
238
+ .request({
239
+ method: 'eth_sendRawTransactionSync',
240
+ params: [updateSigned],
241
+ })
242
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
243
+ expect(updateReceipt.status).toBe('success')
244
+
111
245
  const nonce = await getTransactionCount(client, {
112
246
  address: account,
113
247
  blockTag: 'pending',
@@ -125,11 +259,12 @@ describe('behavior: multisig (TIP-1061)', () => {
125
259
 
126
260
  const spend_signed = TxEnvelopeTempo.serialize(spend, {
127
261
  signature: SignatureEnvelope.from({
128
- genesisConfig,
262
+ initialConfig,
129
263
  signatures: approve({
130
- genesisConfig,
264
+ initialConfig,
131
265
  payload: TxEnvelopeTempo.getSignPayload(spend),
132
- signers: [ownerKeys[1]!, ownerKeys[2]!],
266
+ signers: [replacement],
267
+ version: 1n,
133
268
  }),
134
269
  }),
135
270
  })
@@ -145,7 +280,7 @@ describe('behavior: multisig (TIP-1061)', () => {
145
280
  expect(spend_receipt.from).toBe(account)
146
281
  })
147
282
 
148
- test('behavior: nested multisig owner', async () => {
283
+ test('example: nested ownership', async () => {
149
284
  const child = setup({ count: 1, threshold: 1 })
150
285
  await fundAddress(client, { address: child.account })
151
286
 
@@ -160,10 +295,10 @@ describe('behavior: multisig (TIP-1061)', () => {
160
295
  })
161
296
  const childBootstrapSigned = TxEnvelopeTempo.serialize(childBootstrap, {
162
297
  signature: SignatureEnvelope.from({
163
- genesisConfig: child.genesisConfig,
298
+ initialConfig: child.initialConfig,
164
299
  init: true,
165
300
  signatures: approve({
166
- genesisConfig: child.genesisConfig,
301
+ initialConfig: child.initialConfig,
167
302
  payload: TxEnvelopeTempo.getSignPayload(childBootstrap),
168
303
  signers: child.ownerKeys,
169
304
  }),
@@ -177,12 +312,12 @@ describe('behavior: multisig (TIP-1061)', () => {
177
312
  .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
178
313
  expect(childReceipt.status).toBe('success')
179
314
 
180
- const genesisConfig = MultisigConfig.from({
315
+ const initialConfig = MultisigConfig.from({
181
316
  salt: Hex.random(32),
182
317
  threshold: 1,
183
318
  owners: [{ owner: child.account, weight: 1 }],
184
319
  })
185
- const account = MultisigConfig.getAddress(genesisConfig)
320
+ const account = MultisigConfig.getAddress(initialConfig)
186
321
  await fundAddress(client, { address: account })
187
322
 
188
323
  const bootstrap = TxEnvelopeTempo.from({
@@ -195,20 +330,20 @@ describe('behavior: multisig (TIP-1061)', () => {
195
330
  maxPriorityFeePerGas: Value.fromGwei('10'),
196
331
  })
197
332
  const digest = MultisigConfig.getSignPayload({
198
- genesisConfig,
333
+ initialConfig,
199
334
  payload: TxEnvelopeTempo.getSignPayload(bootstrap),
200
335
  })
201
336
  const nested = SignatureEnvelope.from({
202
- genesisConfig: child.genesisConfig,
337
+ initialConfig: child.initialConfig,
203
338
  signatures: approve({
204
- genesisConfig: child.genesisConfig,
339
+ initialConfig: child.initialConfig,
205
340
  payload: digest,
206
341
  signers: child.ownerKeys,
207
342
  }),
208
343
  })
209
344
  const bootstrapSigned = TxEnvelopeTempo.serialize(bootstrap, {
210
345
  signature: SignatureEnvelope.from({
211
- genesisConfig,
346
+ initialConfig,
212
347
  init: true,
213
348
  signatures: [nested],
214
349
  }),
@@ -222,48 +357,191 @@ describe('behavior: multisig (TIP-1061)', () => {
222
357
  .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
223
358
  expect(receipt.status).toBe('success')
224
359
  expect(receipt.from).toBe(account)
360
+
361
+ const transaction = TxEnvelopeTempo.from({
362
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
363
+ chainId,
364
+ feeToken: '0x20c0000000000000000000000000000000000001',
365
+ nonce: 1n,
366
+ gas: 5_000_000n,
367
+ maxFeePerGas: Value.fromGwei('20'),
368
+ maxPriorityFeePerGas: Value.fromGwei('10'),
369
+ })
370
+ const parentDigest = MultisigConfig.getSignPayload({
371
+ initialConfig,
372
+ payload: TxEnvelopeTempo.getSignPayload(transaction),
373
+ })
374
+ const childSignature = multisigSignature({
375
+ initialConfig: child.initialConfig,
376
+ payload: parentDigest,
377
+ signers: child.ownerKeys,
378
+ })
379
+ const serialized = TxEnvelopeTempo.serialize(transaction, {
380
+ signature: SignatureEnvelope.from({
381
+ initialConfig,
382
+ signatures: [childSignature],
383
+ }),
384
+ })
385
+ const nestedReceipt = (await client
386
+ .request({
387
+ method: 'eth_sendRawTransactionSync',
388
+ params: [serialized],
389
+ })
390
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
391
+ expect(nestedReceipt.status).toBe('success')
392
+ expect(nestedReceipt.from).toBe(account)
225
393
  })
226
394
 
227
- test('behavior: rejects below-threshold approvals', async () => {
228
- const { account, genesisConfig, ownerKeys } = setup({
229
- count: 3,
230
- threshold: 2,
395
+ test('example: fee sponsorship (owners sign first)', async () => {
396
+ const multisig = setup({ count: 2, threshold: 2 })
397
+ const feePayer = createKey()
398
+ await Promise.all([
399
+ bootstrap(multisig),
400
+ fundAddress(client, { address: feePayer.address }),
401
+ ])
402
+
403
+ const transaction = TxEnvelopeTempo.from({
404
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
405
+ chainId,
406
+ feePayerSignature: null,
407
+ nonce: 1n,
408
+ gas: 5_000_000n,
409
+ maxFeePerGas: Value.fromGwei('20'),
410
+ maxPriorityFeePerGas: Value.fromGwei('10'),
411
+ })
412
+ const signedByOwners = TxEnvelopeTempo.from(transaction, {
413
+ signature: multisigSignature({
414
+ initialConfig: multisig.initialConfig,
415
+ payload: TxEnvelopeTempo.getSignPayload(transaction),
416
+ signers: multisig.ownerKeys,
417
+ }),
418
+ })
419
+ const sponsored = TxEnvelopeTempo.from({
420
+ ...signedByOwners,
421
+ feeToken: '0x20c0000000000000000000000000000000000001',
231
422
  })
423
+ const feePayerSignature = Secp256k1.sign({
424
+ payload: TxEnvelopeTempo.getFeePayerSignPayload(sponsored, {
425
+ sender: multisig.account,
426
+ }),
427
+ privateKey: feePayer.privateKey,
428
+ })
429
+ const receipt = await send(
430
+ TxEnvelopeTempo.serialize(sponsored, { feePayerSignature }),
431
+ )
432
+ expect(receipt.status).toBe('success')
433
+ expect(receipt.from).toBe(multisig.account)
434
+ expect(receipt.feePayer).toBe(feePayer.address)
435
+ })
232
436
 
233
- await fundAddress(client, { address: account })
437
+ test('example: fee sponsorship (fee payer signs first)', async () => {
438
+ const multisig = setup({ count: 2, threshold: 2 })
439
+ const feePayer = createKey()
440
+ await Promise.all([
441
+ bootstrap(multisig),
442
+ fundAddress(client, { address: feePayer.address }),
443
+ ])
234
444
 
235
- const bootstrap = TxEnvelopeTempo.from({
445
+ const transaction = TxEnvelopeTempo.from({
236
446
  calls: [{ to: '0x0000000000000000000000000000000000000000' }],
237
447
  chainId,
448
+ feePayerSignature: null,
238
449
  feeToken: '0x20c0000000000000000000000000000000000001',
239
- nonce: 0n,
450
+ nonce: 1n,
240
451
  gas: 5_000_000n,
241
452
  maxFeePerGas: Value.fromGwei('20'),
242
453
  maxPriorityFeePerGas: Value.fromGwei('10'),
243
454
  })
244
-
245
- const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
246
- signature: SignatureEnvelope.from({
247
- genesisConfig,
248
- init: true,
249
- signatures: approve({
250
- genesisConfig,
251
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
252
- signers: [ownerKeys[0]!],
455
+ const feePayerSignature = Secp256k1.sign({
456
+ payload: TxEnvelopeTempo.getFeePayerSignPayload(transaction, {
457
+ sender: multisig.account,
458
+ }),
459
+ privateKey: feePayer.privateKey,
460
+ })
461
+ const sponsored = TxEnvelopeTempo.from(transaction, { feePayerSignature })
462
+ const receipt = await send(
463
+ TxEnvelopeTempo.serialize(sponsored, {
464
+ signature: multisigSignature({
465
+ initialConfig: multisig.initialConfig,
466
+ payload: TxEnvelopeTempo.getSignPayload(sponsored),
467
+ signers: multisig.ownerKeys,
253
468
  }),
254
469
  }),
470
+ )
471
+ expect(receipt.status).toBe('success')
472
+ expect(receipt.from).toBe(multisig.account)
473
+ expect(receipt.feePayer).toBe(feePayer.address)
474
+ })
475
+
476
+ test('example: weighted quorum', async () => {
477
+ const { account, initialConfig, ownerKeys } = setup({
478
+ count: 3,
479
+ threshold: 3,
480
+ weights: [2, 1, 1],
255
481
  })
256
482
 
483
+ await fundAddress(client, { address: account })
484
+
485
+ const transaction = (nonce: bigint) =>
486
+ TxEnvelopeTempo.from({
487
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
488
+ chainId,
489
+ feeToken: '0x20c0000000000000000000000000000000000001',
490
+ nonce,
491
+ gas: 5_000_000n,
492
+ maxFeePerGas: Value.fromGwei('20'),
493
+ maxPriorityFeePerGas: Value.fromGwei('10'),
494
+ })
495
+ const serialize = (
496
+ value: ReturnType<typeof transaction>,
497
+ signers: readonly { privateKey: Hex.Hex }[],
498
+ init = false,
499
+ ) =>
500
+ TxEnvelopeTempo.serialize(value, {
501
+ signature: multisigSignature({
502
+ initialConfig,
503
+ init,
504
+ payload: TxEnvelopeTempo.getSignPayload(value),
505
+ signers,
506
+ }),
507
+ })
508
+
509
+ const bootstrap = transaction(0n)
510
+ const bootstrapReceipt = (await client
511
+ .request({
512
+ method: 'eth_sendRawTransactionSync',
513
+ params: [serialize(bootstrap, [ownerKeys[0]!, ownerKeys[1]!], true)],
514
+ })
515
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
516
+ expect(bootstrapReceipt.status).toBe('success')
517
+
518
+ const valid = transaction(1n)
519
+ const validReceipt = (await client
520
+ .request({
521
+ method: 'eth_sendRawTransactionSync',
522
+ params: [serialize(valid, [ownerKeys[0]!, ownerKeys[2]!])],
523
+ })
524
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
525
+ expect(validReceipt.status).toBe('success')
526
+
527
+ const belowThreshold = transaction(2n)
528
+ await expect(
529
+ client.request({
530
+ method: 'eth_sendRawTransactionSync',
531
+ params: [serialize(belowThreshold, [ownerKeys[1]!, ownerKeys[2]!])],
532
+ }),
533
+ ).rejects.toThrow()
534
+
257
535
  await expect(
258
536
  client.request({
259
537
  method: 'eth_sendRawTransactionSync',
260
- params: [serialized_signed],
538
+ params: [serialize(belowThreshold, ownerKeys)],
261
539
  }),
262
540
  ).rejects.toThrow()
263
541
  })
264
542
 
265
543
  test('behavior: keychain access key (authorize, spend, reject config update)', async () => {
266
- const { account, genesisConfig, ownerKeys } = setup({
544
+ const { account, initialConfig, ownerKeys } = setup({
267
545
  count: 2,
268
546
  threshold: 2,
269
547
  })
@@ -290,10 +568,10 @@ describe('behavior: multisig (TIP-1061)', () => {
290
568
 
291
569
  const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
292
570
  signature: SignatureEnvelope.from({
293
- genesisConfig,
571
+ initialConfig,
294
572
  init: true,
295
573
  signatures: approve({
296
- genesisConfig,
574
+ initialConfig,
297
575
  payload: TxEnvelopeTempo.getSignPayload(bootstrap),
298
576
  signers: ownerKeys,
299
577
  }),
@@ -340,9 +618,9 @@ describe('behavior: multisig (TIP-1061)', () => {
340
618
 
341
619
  const authorize_signed = TxEnvelopeTempo.serialize(authorize, {
342
620
  signature: SignatureEnvelope.from({
343
- genesisConfig,
621
+ initialConfig,
344
622
  signatures: approve({
345
- genesisConfig,
623
+ initialConfig,
346
624
  payload: TxEnvelopeTempo.getSignPayload(authorize),
347
625
  signers: ownerKeys,
348
626
  }),
@@ -411,9 +689,6 @@ describe('behavior: multisig (TIP-1061)', () => {
411
689
  ).toBe(account)
412
690
  }
413
691
 
414
- const updateMultisigConfig = AbiFunction.from(
415
- 'function updateMultisigConfig(uint8 threshold, (address owner, uint8 weight)[] owners)',
416
- )
417
692
  const updateNonce = await getTransactionCount(client, {
418
693
  address: account,
419
694
  blockTag: 'pending',
@@ -422,9 +697,9 @@ describe('behavior: multisig (TIP-1061)', () => {
422
697
  calls: [
423
698
  {
424
699
  to: '0xaacc000000000000000000000000000000000000',
425
- data: AbiFunction.encodeData(updateMultisigConfig, [
426
- genesisConfig.threshold,
427
- genesisConfig.owners,
700
+ data: AbiFunction.encodeData(updateConfig, [
701
+ initialConfig.threshold,
702
+ initialConfig.owners,
428
703
  ]),
429
704
  },
430
705
  ],
@@ -456,127 +731,109 @@ describe('behavior: multisig (TIP-1061)', () => {
456
731
  expect(updateReceipt.status).toBe('reverted')
457
732
  })
458
733
 
459
- test('behavior: rejects owner-signed `keyAuthorization` executed by multisig during bootstrap', async () => {
460
- const { account, genesisConfig, ownerKeys } = setup({
461
- count: 1,
462
- threshold: 1,
463
- })
734
+ test('example: bootstrap and immediate access key use', async () => {
735
+ const multisig = setup({ count: 2, threshold: 2 })
736
+ const accessKey = createKey()
737
+ await fundAddress(client, { address: multisig.account })
464
738
 
465
- await fundAddress(client, { address: account })
466
-
467
- const access = (() => {
468
- const privateKey = Secp256k1.randomPrivateKey()
469
- const address = Address.fromPublicKey(
470
- Secp256k1.getPublicKey({ privateKey }),
471
- )
472
- return { address, privateKey } as const
473
- })()
474
-
475
- const keyAuth = KeyAuthorization.from({
476
- address: access.address,
739
+ const keyAuthorization = KeyAuthorization.from({
740
+ account: multisig.account,
741
+ address: accessKey.address,
477
742
  chainId: BigInt(chainId),
743
+ isAdmin: false,
478
744
  type: 'secp256k1',
479
745
  })
480
- const keyAuth_signed = KeyAuthorization.from(keyAuth, {
481
- signature: SignatureEnvelope.from(
482
- Secp256k1.sign({
483
- payload: KeyAuthorization.getSignPayload(keyAuth),
484
- privateKey: ownerKeys[0]!.privateKey,
485
- }),
486
- ),
746
+ const keyAuthorizationSigned = KeyAuthorization.from(keyAuthorization, {
747
+ signature: multisigSignature({
748
+ initialConfig: multisig.initialConfig,
749
+ init: true,
750
+ payload: KeyAuthorization.getSignPayload(keyAuthorization),
751
+ signers: multisig.ownerKeys,
752
+ }),
487
753
  })
488
-
489
- const bootstrap = TxEnvelopeTempo.from({
754
+ const transaction = TxEnvelopeTempo.from({
490
755
  calls: [{ to: '0x0000000000000000000000000000000000000000' }],
491
756
  chainId,
492
757
  feeToken: '0x20c0000000000000000000000000000000000001',
493
- keyAuthorization: keyAuth_signed,
758
+ keyAuthorization: keyAuthorizationSigned,
494
759
  nonce: 0n,
495
760
  gas: 5_000_000n,
496
761
  maxFeePerGas: Value.fromGwei('20'),
497
762
  maxPriorityFeePerGas: Value.fromGwei('10'),
498
763
  })
499
-
500
- const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
501
- signature: SignatureEnvelope.from({
502
- genesisConfig,
503
- init: true,
504
- signatures: approve({
505
- genesisConfig,
506
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
507
- signers: ownerKeys,
764
+ const receipt = await send(
765
+ TxEnvelopeTempo.serialize(transaction, {
766
+ signature: accessKeySignature({
767
+ accessKey,
768
+ account: multisig.account,
769
+ transaction,
508
770
  }),
509
771
  }),
510
- })
511
-
512
- await expect(
513
- client.request({
514
- method: 'eth_sendRawTransactionSync',
515
- params: [serialized_signed],
516
- }),
517
- ).rejects.toThrow(
518
- 'native multisig transactions cannot carry key_authorization',
519
772
  )
773
+ expect(receipt.status).toBe('success')
774
+ expect(receipt.from).toBe(multisig.account)
520
775
  })
521
776
 
522
- test('behavior: rejects owner-signed `keyAuthorization` executed by access key before bootstrap', async () => {
523
- const { account, ownerKeys } = setup({
524
- count: 1,
525
- threshold: 1,
526
- })
527
-
528
- await fundAddress(client, { address: account })
529
-
530
- const access = (() => {
531
- const privateKey = Secp256k1.randomPrivateKey()
532
- const address = Address.fromPublicKey(
533
- Secp256k1.getPublicKey({ privateKey }),
534
- )
535
- return { address, privateKey } as const
536
- })()
777
+ test('example: bootstrap and subsequent access key use', async () => {
778
+ const multisig = setup({ count: 2, threshold: 2 })
779
+ const accessKey = createKey()
780
+ await fundAddress(client, { address: multisig.account })
537
781
 
538
- const keyAuth = KeyAuthorization.from({
539
- address: access.address,
782
+ const keyAuthorization = KeyAuthorization.from({
783
+ account: multisig.account,
784
+ address: accessKey.address,
540
785
  chainId: BigInt(chainId),
786
+ isAdmin: false,
541
787
  type: 'secp256k1',
542
788
  })
543
- const keyAuth_signed = KeyAuthorization.from(keyAuth, {
544
- signature: SignatureEnvelope.from(
545
- Secp256k1.sign({
546
- payload: KeyAuthorization.getSignPayload(keyAuth),
547
- privateKey: ownerKeys[0]!.privateKey,
548
- }),
549
- ),
789
+ const keyAuthorizationSigned = KeyAuthorization.from(keyAuthorization, {
790
+ signature: multisigSignature({
791
+ initialConfig: multisig.initialConfig,
792
+ payload: KeyAuthorization.getSignPayload(keyAuthorization),
793
+ signers: multisig.ownerKeys,
794
+ }),
550
795
  })
551
-
552
- const bootstrap = TxEnvelopeTempo.from({
796
+ const bootstrapTransaction = TxEnvelopeTempo.from({
553
797
  calls: [{ to: '0x0000000000000000000000000000000000000000' }],
554
798
  chainId,
555
799
  feeToken: '0x20c0000000000000000000000000000000000001',
556
- keyAuthorization: keyAuth_signed,
800
+ keyAuthorization: keyAuthorizationSigned,
557
801
  nonce: 0n,
558
802
  gas: 5_000_000n,
559
803
  maxFeePerGas: Value.fromGwei('20'),
560
804
  maxPriorityFeePerGas: Value.fromGwei('10'),
561
805
  })
562
-
563
- const signature = Secp256k1.sign({
564
- payload: TxEnvelopeTempo.getSignPayload(bootstrap, { from: account }),
565
- privateKey: access.privateKey,
566
- })
567
- const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
568
- signature: SignatureEnvelope.from({
569
- userAddress: account,
570
- inner: SignatureEnvelope.from(signature),
571
- type: 'keychain',
806
+ const bootstrapReceipt = await send(
807
+ TxEnvelopeTempo.serialize(bootstrapTransaction, {
808
+ signature: multisigSignature({
809
+ initialConfig: multisig.initialConfig,
810
+ init: true,
811
+ payload: TxEnvelopeTempo.getSignPayload(bootstrapTransaction),
812
+ signers: multisig.ownerKeys,
813
+ }),
572
814
  }),
573
- })
815
+ )
816
+ expect(bootstrapReceipt.status).toBe('success')
574
817
 
575
- await expect(
576
- client.request({
577
- method: 'eth_sendRawTransactionSync',
578
- params: [serialized_signed],
818
+ const transaction = TxEnvelopeTempo.from({
819
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
820
+ chainId,
821
+ feeToken: '0x20c0000000000000000000000000000000000001',
822
+ nonce: 1n,
823
+ gas: 5_000_000n,
824
+ maxFeePerGas: Value.fromGwei('20'),
825
+ maxPriorityFeePerGas: Value.fromGwei('10'),
826
+ })
827
+ const receipt = await send(
828
+ TxEnvelopeTempo.serialize(transaction, {
829
+ signature: accessKeySignature({
830
+ accessKey,
831
+ account: multisig.account,
832
+ transaction,
833
+ }),
579
834
  }),
580
- ).rejects.toThrow('admin-signed key authorization account mismatch')
835
+ )
836
+ expect(receipt.status).toBe('success')
837
+ expect(receipt.from).toBe(multisig.account)
581
838
  })
582
839
  })