ox 1.7.2 → 1.7.5

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 (142) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/core/AbiEvent.d.ts.map +1 -1
  3. package/dist/core/AbiEvent.js +10 -9
  4. package/dist/core/AbiEvent.js.map +1 -1
  5. package/dist/core/Blobs.d.ts.map +1 -1
  6. package/dist/core/Blobs.js +6 -2
  7. package/dist/core/Blobs.js.map +1 -1
  8. package/dist/core/Signature.d.ts +6 -5
  9. package/dist/core/Signature.d.ts.map +1 -1
  10. package/dist/core/Signature.js +17 -7
  11. package/dist/core/Signature.js.map +1 -1
  12. package/dist/core/Siwe.d.ts.map +1 -1
  13. package/dist/core/Siwe.js +21 -1
  14. package/dist/core/Siwe.js.map +1 -1
  15. package/dist/tempo/KeyAuthorization.d.ts +43 -39
  16. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  17. package/dist/tempo/KeyAuthorization.js +24 -4
  18. package/dist/tempo/KeyAuthorization.js.map +1 -1
  19. package/dist/tempo/MultisigConfig.d.ts +18 -15
  20. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  21. package/dist/tempo/MultisigConfig.js +49 -20
  22. package/dist/tempo/MultisigConfig.js.map +1 -1
  23. package/dist/tempo/MultisigOperation.d.ts +45 -28
  24. package/dist/tempo/MultisigOperation.d.ts.map +1 -1
  25. package/dist/tempo/MultisigOperation.js +81 -131
  26. package/dist/tempo/MultisigOperation.js.map +1 -1
  27. package/dist/tempo/MultisigSimulation.d.ts +85 -0
  28. package/dist/tempo/MultisigSimulation.d.ts.map +1 -0
  29. package/dist/tempo/MultisigSimulation.js +112 -0
  30. package/dist/tempo/MultisigSimulation.js.map +1 -0
  31. package/dist/tempo/RpcSchemaTempo.d.ts +20 -10
  32. package/dist/tempo/RpcSchemaTempo.d.ts.map +1 -1
  33. package/dist/tempo/SignatureEnvelope.d.ts +74 -153
  34. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  35. package/dist/tempo/SignatureEnvelope.js +130 -187
  36. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  37. package/dist/tempo/TransactionRequest.d.ts +9 -7
  38. package/dist/tempo/TransactionRequest.d.ts.map +1 -1
  39. package/dist/tempo/TransactionRequest.js +12 -7
  40. package/dist/tempo/TransactionRequest.js.map +1 -1
  41. package/dist/tempo/index.d.ts +7 -5
  42. package/dist/tempo/index.d.ts.map +1 -1
  43. package/dist/tempo/index.js +7 -5
  44. package/dist/tempo/index.js.map +1 -1
  45. package/dist/zod/tempo/AuthorizationTempo.d.ts +360 -140
  46. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  47. package/dist/zod/tempo/KeyAuthorization.d.ts +247 -487
  48. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  49. package/dist/zod/tempo/KeyAuthorization.js +29 -31
  50. package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
  51. package/dist/zod/tempo/MultisigOperation.d.ts +0 -10
  52. package/dist/zod/tempo/MultisigOperation.d.ts.map +1 -1
  53. package/dist/zod/tempo/MultisigOperation.js +0 -3
  54. package/dist/zod/tempo/MultisigOperation.js.map +1 -1
  55. package/dist/zod/tempo/MultisigSimulation.d.ts +52 -0
  56. package/dist/zod/tempo/MultisigSimulation.d.ts.map +1 -0
  57. package/dist/zod/tempo/MultisigSimulation.js +44 -0
  58. package/dist/zod/tempo/MultisigSimulation.js.map +1 -0
  59. package/dist/zod/tempo/RpcSchemaTempo.d.ts +357 -422
  60. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  61. package/dist/zod/tempo/RpcSchemaTempo.js +19 -10
  62. package/dist/zod/tempo/RpcSchemaTempo.js.map +1 -1
  63. package/dist/zod/tempo/SignatureEnvelope.d.ts +108 -42
  64. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  65. package/dist/zod/tempo/SignatureEnvelope.js +21 -19
  66. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  67. package/dist/zod/tempo/Transaction.d.ts +678 -666
  68. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  69. package/dist/zod/tempo/TransactionRequest.d.ts +580 -619
  70. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  71. package/dist/zod/tempo/TransactionRequest.js +18 -10
  72. package/dist/zod/tempo/TransactionRequest.js.map +1 -1
  73. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +327 -213
  74. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  75. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts +105 -3
  76. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts.map +1 -1
  77. package/dist/zod/tempo/z.d.ts +1 -1
  78. package/dist/zod/tempo/z.d.ts.map +1 -1
  79. package/dist/zod/tempo/z.js +1 -1
  80. package/dist/zod/tempo/z.js.map +1 -1
  81. package/package.json +9 -9
  82. package/src/core/AbiEvent.ts +11 -9
  83. package/src/core/Blobs.ts +6 -2
  84. package/src/core/Signature.ts +41 -11
  85. package/src/core/Siwe.ts +20 -1
  86. package/src/core/_test/AbiEvent.test.ts +45 -0
  87. package/src/core/_test/Blobs.test.ts +22 -0
  88. package/src/core/_test/Signature.test.ts +166 -0
  89. package/src/core/_test/Siwe.test.ts +100 -1
  90. package/src/tempo/AuthorizationTempo.test.ts +8 -0
  91. package/src/tempo/KeyAuthorization.test-d.ts +16 -74
  92. package/src/tempo/KeyAuthorization.test.ts +113 -14
  93. package/src/tempo/KeyAuthorization.ts +110 -66
  94. package/src/tempo/MultisigConfig.test.ts +44 -20
  95. package/src/tempo/MultisigConfig.ts +65 -23
  96. package/src/tempo/MultisigOperation.test-d.ts +8 -6
  97. package/src/tempo/MultisigOperation.test.ts +187 -591
  98. package/src/tempo/MultisigOperation.ts +129 -205
  99. package/src/tempo/MultisigSimulation.test-d.ts +33 -0
  100. package/src/tempo/MultisigSimulation.test.ts +194 -0
  101. package/src/tempo/MultisigSimulation.ts +192 -0
  102. package/src/tempo/RpcSchemaTempo.test-d.ts +45 -10
  103. package/src/tempo/RpcSchemaTempo.ts +15 -6
  104. package/src/tempo/SignatureEnvelope.test-d.ts +27 -101
  105. package/src/tempo/SignatureEnvelope.test.ts +330 -533
  106. package/src/tempo/SignatureEnvelope.ts +198 -310
  107. package/src/tempo/Transaction.test.ts +61 -0
  108. package/src/tempo/TransactionRequest.test-d.ts +7 -7
  109. package/src/tempo/TransactionRequest.test.ts +81 -223
  110. package/src/tempo/TransactionRequest.ts +34 -13
  111. package/src/tempo/index.ts +7 -5
  112. package/src/tempo/multisig.e2e.test.ts +329 -818
  113. package/src/version.ts +1 -1
  114. package/src/zod/tempo/KeyAuthorization.ts +50 -39
  115. package/src/zod/tempo/MultisigOperation.ts +0 -3
  116. package/src/zod/tempo/MultisigSimulation.ts +53 -0
  117. package/src/zod/tempo/RpcSchemaTempo.ts +20 -10
  118. package/src/zod/tempo/SignatureEnvelope.ts +46 -59
  119. package/src/zod/tempo/TransactionRequest.ts +28 -14
  120. package/src/zod/tempo/_test/KeyAuthorization.test.ts +66 -30
  121. package/src/zod/tempo/_test/MultisigOperation.test.ts +0 -2
  122. package/src/zod/tempo/_test/MultisigSimulation.test-d.ts +13 -0
  123. package/src/zod/tempo/_test/MultisigSimulation.test.ts +53 -0
  124. package/src/zod/tempo/_test/RpcSchemaTempo.test-d.ts +19 -2
  125. package/src/zod/tempo/_test/RpcSchemaTempo.test.ts +46 -4
  126. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +108 -129
  127. package/src/zod/tempo/_test/TransactionRequest.test.ts +17 -168
  128. package/src/zod/tempo/z.ts +1 -1
  129. package/dist/tempo/MultisigWitness.d.ts +0 -106
  130. package/dist/tempo/MultisigWitness.d.ts.map +0 -1
  131. package/dist/tempo/MultisigWitness.js +0 -118
  132. package/dist/tempo/MultisigWitness.js.map +0 -1
  133. package/dist/zod/tempo/MultisigWitness.d.ts +0 -152
  134. package/dist/zod/tempo/MultisigWitness.d.ts.map +0 -1
  135. package/dist/zod/tempo/MultisigWitness.js +0 -76
  136. package/dist/zod/tempo/MultisigWitness.js.map +0 -1
  137. package/src/tempo/MultisigWitness.test-d.ts +0 -42
  138. package/src/tempo/MultisigWitness.test.ts +0 -288
  139. package/src/tempo/MultisigWitness.ts +0 -210
  140. package/src/zod/tempo/MultisigWitness.ts +0 -101
  141. package/src/zod/tempo/_test/MultisigWitness.test-d.ts +0 -13
  142. package/src/zod/tempo/_test/MultisigWitness.test.ts +0 -149
@@ -1,866 +1,377 @@
1
1
  import { AbiFunction, Address, Hex, Secp256k1, Value } from 'ox'
2
- import { getTransactionCount } from 'viem/actions'
3
2
  import { describe, expect, test } from 'vp/test'
4
3
  import { chain, client, fundAddress } from '../../test/tempo/config.js'
5
- import { KeyAuthorization, MultisigConfig, SignatureEnvelope } from './index.js'
4
+ import { factory } from '../../test/tempo/multisig.js'
5
+ import {
6
+ KeyAuthorization,
7
+ MultisigConfig,
8
+ MultisigOperation,
9
+ SignatureEnvelope,
10
+ } from './index.js'
6
11
  import * as Transaction from './Transaction.js'
7
12
  import * as TransactionReceipt from './TransactionReceipt.js'
13
+ import * as TransactionRequest from './TransactionRequest.js'
8
14
  import * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
9
15
 
10
- const chainId = chain.id
16
+ const precompile = '0xaacc000000000000000000000000000000000000'
11
17
  const updateConfig = AbiFunction.from(
12
18
  'function updateConfig((bytes32 salt, uint64 version, uint8 threshold, (address owner, uint8 weight)[] owners) current, uint8 threshold, (address owner, uint8 weight)[] owners)',
13
19
  )
14
20
 
15
- describe('behavior: multisig (TIP-1061)', () => {
16
- function createKey() {
21
+ async function setup() {
22
+ const keys = Array.from({ length: 3 }, () => {
17
23
  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
30
- const ownerKeys = Array.from({ length: count }, () => {
31
- const privateKey = Secp256k1.randomPrivateKey()
32
- const address = Address.fromPublicKey(
33
- Secp256k1.getPublicKey({ privateKey }),
34
- )
35
- return { address, privateKey } as const
36
- }).sort((a, b) => a.address.localeCompare(b.address))
37
-
38
- const initialConfig = MultisigConfig.from({
39
- salt: Hex.random(32),
40
- threshold,
41
- owners: ownerKeys.map((key, index) => ({
42
- owner: key.address,
43
- weight: weights[index]!,
44
- })),
45
- })
46
- const account = MultisigConfig.getAddress(initialConfig)
47
-
48
- return { account, initialConfig, ownerKeys } as const
49
- }
50
-
51
- function approve(parameters: {
52
- address?: Address.Address | undefined
53
- config: MultisigConfig.Config
54
- payload: Hex.Hex
55
- signers: readonly { privateKey: Hex.Hex }[]
56
- }) {
57
- const {
58
- address = MultisigConfig.getAddress(parameters.config),
59
- config,
60
- payload,
61
- signers,
62
- } = parameters
63
- const digest = MultisigConfig.getSignPayload({
64
- account: address,
65
- config,
66
- payload,
67
- })
68
- const signatures = signers.map((signer) =>
69
- SignatureEnvelope.from(
70
- Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
71
- ),
72
- )
73
- // The node requires approvals ordered by recovered owner address.
74
- return SignatureEnvelope.sortMultisigApprovals({
75
- account: address,
76
- config,
77
- payload,
78
- signatures,
79
- })
80
- }
81
-
82
- async function send(serialized: Hex.Hex) {
83
- return (await client
84
- .request({
85
- method: 'eth_sendRawTransactionSync',
86
- params: [serialized],
87
- })
88
- .then((transaction) => TransactionReceipt.fromRpc(transaction as any)))!
89
- }
24
+ return {
25
+ privateKey,
26
+ address: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })),
27
+ }
28
+ })
29
+ const config: MultisigConfig.Config = MultisigConfig.from({
30
+ salt: Hex.random(32),
31
+ threshold: 2,
32
+ owners: keys.map((key) => ({ owner: key.address, weight: 1 })),
33
+ })
34
+ const account = MultisigConfig.getAddress(config, { factory })
35
+ await fundAddress(client, { address: account })
36
+ return { account, config, keys }
37
+ }
38
+
39
+ function transaction(
40
+ nonce: bigint,
41
+ calls: TxEnvelopeTempo.TxEnvelopeTempo['calls'] = [
42
+ { to: '0x0000000000000000000000000000000000000000' },
43
+ ],
44
+ ) {
45
+ return TxEnvelopeTempo.from({
46
+ calls,
47
+ chainId: chain.id,
48
+ feeToken: '0x20c0000000000000000000000000000000000001',
49
+ nonce,
50
+ gas: 5_000_000n,
51
+ maxFeePerGas: Value.fromGwei('20'),
52
+ maxPriorityFeePerGas: Value.fromGwei('10'),
53
+ })
54
+ }
90
55
 
91
- async function bootstrap(multisig: ReturnType<typeof setup>) {
92
- await fundAddress(client, { address: multisig.account })
93
- const transaction = TxEnvelopeTempo.from({
94
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
95
- chainId,
96
- feeToken: '0x20c0000000000000000000000000000000000001',
97
- nonce: 0n,
98
- gas: 5_000_000n,
99
- maxFeePerGas: Value.fromGwei('20'),
100
- maxPriorityFeePerGas: Value.fromGwei('10'),
101
- })
102
- const receipt = await send(
103
- TxEnvelopeTempo.serialize(transaction, {
104
- signature: SignatureEnvelope.from({
105
- account: multisig.account,
106
- config: multisig.initialConfig,
107
- signatures: approve({
108
- config: multisig.initialConfig,
109
- payload: TxEnvelopeTempo.getSignPayload(transaction),
110
- signers: multisig.ownerKeys,
111
- }),
112
- }),
113
- }),
114
- )
115
- expect(receipt.status).toBe('success')
56
+ function sign(account: Awaited<ReturnType<typeof setup>>, payload: Hex.Hex) {
57
+ const value = {
58
+ account: account.account,
59
+ config: account.config,
60
+ payload,
116
61
  }
117
-
118
- function accessKeySignature(parameters: {
119
- accessKey: { privateKey: Hex.Hex }
120
- account: Address.Address
121
- transaction: TxEnvelopeTempo.TxEnvelopeTempo
122
- }) {
123
- const { accessKey, account, transaction } = parameters
124
- return SignatureEnvelope.from({
125
- inner: SignatureEnvelope.from(
126
- Secp256k1.sign({
127
- payload: TxEnvelopeTempo.getSignPayload(transaction, {
128
- from: account,
129
- }),
130
- privateKey: accessKey.privateKey,
131
- }),
132
- ),
133
- type: 'keychain',
134
- userAddress: account,
135
- })
136
- }
137
-
138
- test('examples: bootstrap, initialized, and configuration rotation', async () => {
139
- const { account, initialConfig, ownerKeys } = setup({
140
- count: 3,
141
- threshold: 2,
142
- })
143
- const replacement = createKey()
144
- const rotatedConfig = MultisigConfig.from({
145
- owners: [{ owner: replacement.address, weight: 1 }],
146
- salt: initialConfig.salt,
147
- threshold: 1,
148
- version: 1,
149
- })
150
-
151
- await fundAddress(client, { address: account })
152
-
153
- const bootstrap = TxEnvelopeTempo.from({
154
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
155
- chainId,
156
- feeToken: '0x20c0000000000000000000000000000000000001',
157
- nonce: 0n,
158
- gas: 5_000_000n,
159
- maxFeePerGas: Value.fromGwei('20'),
160
- maxPriorityFeePerGas: Value.fromGwei('10'),
161
- })
162
-
163
- const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
164
- signature: SignatureEnvelope.from({
165
- account,
166
- config: initialConfig,
167
- signatures: approve({
168
- config: initialConfig,
169
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
170
- signers: [ownerKeys[0]!, ownerKeys[1]!],
171
- }),
172
- }),
173
- })
174
-
175
- const bootstrap_receipt = (await client
176
- .request({
177
- method: 'eth_sendRawTransactionSync',
178
- params: [bootstrap_signed],
62
+ const digest = MultisigConfig.getSignPayload(value)
63
+ return SignatureEnvelope.from({
64
+ account: account.account,
65
+ config: account.config,
66
+ signatures: SignatureEnvelope.sortMultisigApprovals({
67
+ ...value,
68
+ signatures: account.keys
69
+ .slice(0, 2)
70
+ .map((key) =>
71
+ SignatureEnvelope.from(
72
+ Secp256k1.sign({ payload: digest, privateKey: key.privateKey }),
73
+ ),
74
+ ),
75
+ }),
76
+ })
77
+ }
78
+
79
+ async function submit(
80
+ tx: TxEnvelopeTempo.TxEnvelopeTempo,
81
+ signature: SignatureEnvelope.SignatureEnvelope,
82
+ ) {
83
+ const result = await client.request({
84
+ method: 'eth_sendRawTransactionSync',
85
+ params: [TxEnvelopeTempo.serialize(tx, { signature })],
86
+ })
87
+ return TransactionReceipt.fromRpc(result as unknown as TransactionReceipt.Rpc)
88
+ }
89
+
90
+ describe('behavior: configurable accounts', () => {
91
+ test('registers, spends, and round-trips the RPC signature', async () => {
92
+ const account = await setup()
93
+ for (const nonce of [0n, 1n]) {
94
+ const tx = transaction(nonce)
95
+ const signature = sign(account, TxEnvelopeTempo.getSignPayload(tx))
96
+ const receipt = await submit(tx, signature)
97
+ expect(receipt.status).toMatchInlineSnapshot('"success"')
98
+ expect(receipt.from).toBe(account.account)
99
+ const rpc = await client.request({
100
+ method: 'eth_getTransactionByHash',
101
+ params: [receipt.transactionHash],
179
102
  })
180
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
181
- expect(bootstrap_receipt).toBeDefined()
182
- expect(bootstrap_receipt.status).toBe('success')
183
- expect(bootstrap_receipt.from).toBe(account)
184
-
185
- {
186
- const response = await client
187
- .request({
188
- method: 'eth_getTransactionByHash',
189
- params: [bootstrap_receipt.transactionHash],
190
- })
191
- .then((tx) => Transaction.fromRpc(tx as any))
192
- if (!response) throw new Error()
193
- expect(response.from).toBe(account)
194
- expect(response.signature?.type).toBe('multisig')
195
- expect(
196
- (response.signature as SignatureEnvelope.Multisig | undefined)?.config,
197
- ).toEqual(initialConfig)
103
+ const returned = Transaction.fromRpc(rpc as unknown as Transaction.Rpc)
104
+ expect(returned.signature).toEqual(signature)
198
105
  }
199
-
200
- const updateNonce = await getTransactionCount(client, {
201
- address: account,
202
- blockTag: 'pending',
203
- })
204
- const update = TxEnvelopeTempo.from({
205
- calls: [
206
- {
207
- to: '0xaacc000000000000000000000000000000000000',
208
- data: AbiFunction.encodeData(updateConfig, [
209
- initialConfig,
210
- rotatedConfig.threshold,
211
- rotatedConfig.owners,
212
- ]),
213
- },
214
- ],
215
- chainId,
216
- feeToken: '0x20c0000000000000000000000000000000000001',
217
- nonce: BigInt(updateNonce),
218
- gas: 5_000_000n,
219
- maxFeePerGas: Value.fromGwei('20'),
220
- maxPriorityFeePerGas: Value.fromGwei('10'),
221
- })
222
- const updateSigned = TxEnvelopeTempo.serialize(update, {
223
- signature: SignatureEnvelope.from({
224
- account,
225
- config: initialConfig,
226
- signatures: approve({
227
- config: initialConfig,
228
- payload: TxEnvelopeTempo.getSignPayload(update),
229
- signers: [ownerKeys[0]!, ownerKeys[1]!],
230
- }),
231
- }),
232
- })
233
- const updateReceipt = (await client
234
- .request({
235
- method: 'eth_sendRawTransactionSync',
236
- params: [updateSigned],
237
- })
238
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
239
- expect(updateReceipt.status).toBe('success')
240
-
241
- const nonce = await getTransactionCount(client, {
242
- address: account,
243
- blockTag: 'pending',
244
- })
245
-
246
- const spend = TxEnvelopeTempo.from({
247
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
248
- chainId,
249
- feeToken: '0x20c0000000000000000000000000000000000001',
250
- nonce: BigInt(nonce),
251
- gas: 5_000_000n,
252
- maxFeePerGas: Value.fromGwei('20'),
253
- maxPriorityFeePerGas: Value.fromGwei('10'),
254
- })
255
-
256
- const spend_signed = TxEnvelopeTempo.serialize(spend, {
257
- signature: SignatureEnvelope.from({
258
- account,
259
- config: rotatedConfig,
260
- signatures: approve({
261
- address: account,
262
- config: rotatedConfig,
263
- payload: TxEnvelopeTempo.getSignPayload(spend),
264
- signers: [replacement],
265
- }),
266
- }),
267
- })
268
-
269
- const spend_receipt = (await client
270
- .request({
271
- method: 'eth_sendRawTransactionSync',
272
- params: [spend_signed],
273
- })
274
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
275
- expect(spend_receipt).toBeDefined()
276
- expect(spend_receipt.status).toBe('success')
277
- expect(spend_receipt.from).toBe(account)
278
106
  })
279
107
 
280
- test('example: nested ownership', async () => {
281
- const child = setup({ count: 1, threshold: 1 })
282
- await fundAddress(client, { address: child.account })
283
-
284
- const childBootstrap = TxEnvelopeTempo.from({
285
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
286
- chainId,
287
- feeToken: '0x20c0000000000000000000000000000000000001',
288
- nonce: 0n,
289
- gas: 5_000_000n,
290
- maxFeePerGas: Value.fromGwei('20'),
291
- maxPriorityFeePerGas: Value.fromGwei('10'),
292
- })
293
- const childBootstrapSigned = TxEnvelopeTempo.serialize(childBootstrap, {
294
- signature: SignatureEnvelope.from({
295
- account: child.account,
296
- config: child.initialConfig,
297
- signatures: approve({
298
- config: child.initialConfig,
299
- payload: TxEnvelopeTempo.getSignPayload(childBootstrap),
300
- signers: child.ownerKeys,
301
- }),
302
- }),
303
- })
304
- const childReceipt = (await client
305
- .request({
306
- method: 'eth_sendRawTransactionSync',
307
- params: [childBootstrapSigned],
308
- })
309
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
310
- expect(childReceipt.status).toBe('success')
311
-
312
- const initialConfig = MultisigConfig.from({
313
- salt: Hex.random(32),
314
- threshold: 1,
315
- owners: [{ owner: child.account, weight: 1 }],
316
- })
317
- const account = MultisigConfig.getAddress(initialConfig)
318
- await fundAddress(client, { address: account })
319
-
320
- const bootstrap = TxEnvelopeTempo.from({
321
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
322
- chainId,
323
- feeToken: '0x20c0000000000000000000000000000000000001',
324
- nonce: 0n,
325
- gas: 5_000_000n,
326
- maxFeePerGas: Value.fromGwei('20'),
327
- maxPriorityFeePerGas: Value.fromGwei('10'),
328
- })
329
- const digest = MultisigConfig.getSignPayload({
330
- account,
331
- config: initialConfig,
332
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
333
- })
334
- const nested = SignatureEnvelope.from({
335
- account: child.account,
336
- config: child.initialConfig,
337
- signatures: approve({
338
- config: child.initialConfig,
339
- payload: digest,
340
- signers: child.ownerKeys,
341
- }),
342
- })
343
- const bootstrapSigned = TxEnvelopeTempo.serialize(bootstrap, {
344
- signature: SignatureEnvelope.from({
345
- account,
346
- config: initialConfig,
347
- signatures: [nested],
348
- }),
349
- })
350
-
351
- const receipt = (await client
352
- .request({
353
- method: 'eth_sendRawTransactionSync',
354
- params: [bootstrapSigned],
355
- })
356
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
357
- expect(receipt.status).toBe('success')
358
- expect(receipt.from).toBe(account)
359
-
360
- const transaction = TxEnvelopeTempo.from({
361
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
362
- chainId,
363
- feeToken: '0x20c0000000000000000000000000000000000001',
364
- nonce: 1n,
365
- gas: 5_000_000n,
366
- maxFeePerGas: Value.fromGwei('20'),
367
- maxPriorityFeePerGas: Value.fromGwei('10'),
368
- })
369
- const parentDigest = MultisigConfig.getSignPayload({
370
- account,
371
- config: initialConfig,
372
- payload: TxEnvelopeTempo.getSignPayload(transaction),
373
- })
374
- const childSignature = SignatureEnvelope.from({
375
- account: child.account,
376
- config: child.initialConfig,
377
- signatures: approve({
378
- config: child.initialConfig,
379
- payload: parentDigest,
380
- signers: child.ownerKeys,
381
- }),
382
- })
383
- const serialized = TxEnvelopeTempo.serialize(transaction, {
384
- signature: SignatureEnvelope.from({
385
- account,
386
- config: initialConfig,
387
- signatures: [childSignature],
388
- }),
389
- })
390
- const nestedReceipt = (await client
391
- .request({
392
- method: 'eth_sendRawTransactionSync',
393
- params: [serialized],
394
- })
395
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
396
- expect(nestedReceipt.status).toBe('success')
397
- expect(nestedReceipt.from).toBe(account)
108
+ test('selects operation approvals and submits the serialized transaction', async () => {
109
+ const account = await setup()
110
+ const tx = transaction(0n)
111
+ const serialized = TxEnvelopeTempo.serialize(tx)
112
+ const hash = MultisigOperation.getHash({
113
+ account: account.account,
114
+ config: account.config,
115
+ transaction: serialized,
116
+ type: 'transaction',
117
+ })
118
+ const selected = await MultisigOperation.selectApprovals({
119
+ account: account.account,
120
+ approvals: account.keys.map(({ privateKey }) =>
121
+ SignatureEnvelope.serialize(
122
+ SignatureEnvelope.from(Secp256k1.sign({ payload: hash, privateKey })),
123
+ ),
124
+ ),
125
+ config: account.config,
126
+ hash,
127
+ })
128
+ const operation = MultisigOperation.from({
129
+ account: account.account,
130
+ approvals: selected.approvals,
131
+ config: account.config,
132
+ createdAt: 1,
133
+ hash,
134
+ signatureCount: selected.signatureCount,
135
+ status: 'pending',
136
+ threshold: selected.threshold,
137
+ transaction: serialized,
138
+ type: 'transaction',
139
+ updatedAt: 1,
140
+ weight: selected.weight,
141
+ })
142
+ const signed = MultisigOperation.serializeTransaction(operation, {
143
+ approvals: selected.selectedApprovals,
144
+ })
145
+ const result = await client.request({
146
+ method: 'eth_sendRawTransactionSync',
147
+ params: [signed],
148
+ })
149
+ const receipt = TransactionReceipt.fromRpc(
150
+ result as unknown as TransactionReceipt.Rpc,
151
+ )
152
+ expect(receipt.status).toMatchInlineSnapshot('"success"')
153
+ expect(receipt.from).toBe(account.account)
398
154
  })
399
155
 
400
- test('example: fee sponsorship (owners sign first)', async () => {
401
- const multisig = setup({ count: 2, threshold: 2 })
402
- const feePayer = createKey()
403
- await Promise.all([
404
- bootstrap(multisig),
405
- fundAddress(client, { address: feePayer.address }),
406
- ])
407
-
408
- const transaction = TxEnvelopeTempo.from({
409
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
410
- chainId,
411
- feePayerSignature: null,
412
- nonce: 1n,
156
+ test('simulates registration with the full config without persisting it', async () => {
157
+ const account = await setup()
158
+ const data = AbiFunction.encodeData(
159
+ AbiFunction.from(
160
+ 'function getConfigCommitment(address account) view returns (bytes32)',
161
+ ),
162
+ [account.account],
163
+ )
164
+ const request = TransactionRequest.toRpc({
165
+ from: account.account,
166
+ to: precompile,
167
+ data,
413
168
  gas: 5_000_000n,
414
- maxFeePerGas: Value.fromGwei('20'),
415
- maxPriorityFeePerGas: Value.fromGwei('10'),
416
- })
417
- const signedByOwners = TxEnvelopeTempo.from(transaction, {
418
- signature: SignatureEnvelope.from({
419
- account: multisig.account,
420
- config: multisig.initialConfig,
421
- signatures: approve({
422
- config: multisig.initialConfig,
423
- payload: TxEnvelopeTempo.getSignPayload(transaction),
424
- signers: multisig.ownerKeys,
425
- }),
426
- }),
427
- })
428
- const sponsored = TxEnvelopeTempo.from({
429
- ...signedByOwners,
430
169
  feeToken: '0x20c0000000000000000000000000000000000001',
431
- })
432
- const feePayerSignature = Secp256k1.sign({
433
- payload: TxEnvelopeTempo.getFeePayerSignPayload(sponsored, {
434
- sender: multisig.account,
170
+ multisigSimulation: {
171
+ config: account.config,
172
+ approvals: account.config.owners
173
+ .slice(0, 2)
174
+ .map(({ owner }) => ({ owner, keyType: 'secp256k1' })),
175
+ },
176
+ })
177
+ const simulated = await client.request({
178
+ method: 'eth_call',
179
+ params: [request as never, 'latest'],
180
+ })
181
+ expect(simulated).not.toBe(MultisigConfig.zeroSalt)
182
+ const estimate = await client.request({
183
+ method: 'eth_estimateGas',
184
+ params: [request as never],
185
+ })
186
+ expect(Hex.toBigInt(estimate)).toBeGreaterThan(0n)
187
+ const persisted = await client.request({
188
+ method: 'eth_call',
189
+ params: [{ to: precompile, data }, 'latest'],
190
+ })
191
+ expect(persisted).toBe(MultisigConfig.zeroSalt)
192
+ const tx = transaction(0n)
193
+ await submit(tx, sign(account, TxEnvelopeTempo.getSignPayload(tx)))
194
+ expect(
195
+ await client.request({
196
+ method: 'eth_call',
197
+ params: [{ to: precompile, data }, 'latest'],
435
198
  }),
436
- privateKey: feePayer.privateKey,
437
- })
438
- const receipt = await send(
439
- TxEnvelopeTempo.serialize(sponsored, { feePayerSignature }),
440
- )
441
- expect(receipt.status).toBe('success')
442
- expect(receipt.from).toBe(multisig.account)
443
- expect(receipt.feePayer).toBe(feePayer.address)
199
+ ).toBe(simulated)
444
200
  })
445
201
 
446
- test('example: fee sponsorship (fee payer signs first)', async () => {
447
- const multisig = setup({ count: 2, threshold: 2 })
448
- const feePayer = createKey()
449
- await Promise.all([
450
- bootstrap(multisig),
451
- fundAddress(client, { address: feePayer.address }),
202
+ test('rotates config and rejects stale witnesses and signatures', async () => {
203
+ const account = await setup()
204
+ const replacement = account
205
+ const tx = transaction(0n, [
206
+ {
207
+ to: precompile,
208
+ data: AbiFunction.encodeData(updateConfig, [
209
+ { ...account.config, salt: account.config.salt!, version: 0n },
210
+ 2,
211
+ replacement.config.owners,
212
+ ]),
213
+ },
452
214
  ])
453
-
454
- const transaction = TxEnvelopeTempo.from({
455
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
456
- chainId,
457
- feePayerSignature: null,
458
- feeToken: '0x20c0000000000000000000000000000000000001',
459
- nonce: 1n,
460
- gas: 5_000_000n,
461
- maxFeePerGas: Value.fromGwei('20'),
462
- maxPriorityFeePerGas: Value.fromGwei('10'),
463
- })
464
- const feePayerSignature = Secp256k1.sign({
465
- payload: TxEnvelopeTempo.getFeePayerSignPayload(transaction, {
466
- sender: multisig.account,
467
- }),
468
- privateKey: feePayer.privateKey,
469
- })
470
- const sponsored = TxEnvelopeTempo.from(transaction, { feePayerSignature })
471
- const receipt = await send(
472
- TxEnvelopeTempo.serialize(sponsored, {
473
- signature: SignatureEnvelope.from({
474
- account: multisig.account,
475
- config: multisig.initialConfig,
476
- signatures: approve({
477
- config: multisig.initialConfig,
478
- payload: TxEnvelopeTempo.getSignPayload(sponsored),
479
- signers: multisig.ownerKeys,
480
- }),
481
- }),
482
- }),
483
- )
484
- expect(receipt.status).toBe('success')
485
- expect(receipt.from).toBe(multisig.account)
486
- expect(receipt.feePayer).toBe(feePayer.address)
487
- })
488
-
489
- test('example: weighted quorum', async () => {
490
- const { account, initialConfig, ownerKeys } = setup({
491
- count: 3,
492
- threshold: 3,
493
- weights: [2, 1, 1],
494
- })
495
-
496
- await fundAddress(client, { address: account })
497
-
498
- const transaction = (nonce: bigint) =>
499
- TxEnvelopeTempo.from({
500
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
501
- chainId,
502
- feeToken: '0x20c0000000000000000000000000000000000001',
503
- nonce,
504
- gas: 5_000_000n,
505
- maxFeePerGas: Value.fromGwei('20'),
506
- maxPriorityFeePerGas: Value.fromGwei('10'),
507
- })
508
- const serialize = (
509
- value: ReturnType<typeof transaction>,
510
- signers: readonly { privateKey: Hex.Hex }[],
511
- ) =>
512
- TxEnvelopeTempo.serialize(value, {
513
- signature: SignatureEnvelope.from({
514
- account,
515
- config: initialConfig,
516
- signatures: approve({
517
- config: initialConfig,
518
- payload: TxEnvelopeTempo.getSignPayload(value),
519
- signers,
520
- }),
521
- }),
522
- })
523
-
524
- const bootstrap = transaction(0n)
525
- const bootstrapReceipt = (await client
526
- .request({
527
- method: 'eth_sendRawTransactionSync',
528
- params: [serialize(bootstrap, [ownerKeys[0]!, ownerKeys[1]!])],
529
- })
530
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
531
- expect(bootstrapReceipt.status).toBe('success')
532
-
533
- const valid = transaction(1n)
534
- const validReceipt = (await client
535
- .request({
536
- method: 'eth_sendRawTransactionSync',
537
- params: [serialize(valid, [ownerKeys[0]!, ownerKeys[2]!])],
538
- })
539
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
540
- expect(validReceipt.status).toBe('success')
541
-
542
- const belowThreshold = transaction(2n)
215
+ expect(
216
+ (await submit(tx, sign(account, TxEnvelopeTempo.getSignPayload(tx))))
217
+ .status,
218
+ ).toMatchInlineSnapshot('"success"')
219
+ const spend = transaction(1n)
220
+ const old = sign(account, TxEnvelopeTempo.getSignPayload(spend))
221
+ await expect(submit(spend, old)).rejects.toThrow(/commitment mismatch/)
222
+ const rotated = {
223
+ ...replacement,
224
+ account: account.account,
225
+ config: { ...replacement.config, salt: account.config.salt, version: 1n },
226
+ }
543
227
  await expect(
544
- client.request({
545
- method: 'eth_sendRawTransactionSync',
546
- params: [serialize(belowThreshold, [ownerKeys[1]!, ownerKeys[2]!])],
547
- }),
228
+ submit(spend, { ...old, config: rotated.config }),
548
229
  ).rejects.toThrow()
230
+ expect(
231
+ (
232
+ await submit(
233
+ spend,
234
+ sign(rotated, TxEnvelopeTempo.getSignPayload(spend)),
235
+ )
236
+ ).status,
237
+ ).toMatchInlineSnapshot('"success"')
238
+ })
549
239
 
240
+ test('rejects insufficient approvals without registering the account', async () => {
241
+ const account = await setup()
242
+ const tx = transaction(0n)
243
+ const signature = sign(account, TxEnvelopeTempo.getSignPayload(tx))
550
244
  await expect(
551
- client.request({
552
- method: 'eth_sendRawTransactionSync',
553
- params: [serialize(belowThreshold, ownerKeys)],
245
+ submit(tx, {
246
+ ...signature,
247
+ signatures: signature.signatures.slice(0, 1),
554
248
  }),
555
- ).rejects.toThrow()
249
+ ).rejects.toThrow(/threshold|weight|quorum/)
250
+ expect((await submit(tx, signature)).status).toMatchInlineSnapshot(
251
+ '"success"',
252
+ )
556
253
  })
557
254
 
558
- test('behavior: keychain access key (authorize, spend, reject config update)', async () => {
559
- const { account, initialConfig, ownerKeys } = setup({
560
- count: 2,
561
- threshold: 2,
562
- })
563
-
564
- await fundAddress(client, { address: account })
565
-
566
- const access = (() => {
567
- const privateKey = Secp256k1.randomPrivateKey()
568
- const address = Address.fromPublicKey(
569
- Secp256k1.getPublicKey({ privateKey }),
570
- )
571
- return { address, privateKey } as const
572
- })()
573
-
574
- const bootstrap = TxEnvelopeTempo.from({
575
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
576
- chainId,
577
- feeToken: '0x20c0000000000000000000000000000000000001',
578
- nonce: 0n,
579
- gas: 5_000_000n,
580
- maxFeePerGas: Value.fromGwei('20'),
581
- maxPriorityFeePerGas: Value.fromGwei('10'),
582
- })
583
-
584
- const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
585
- signature: SignatureEnvelope.from({
586
- account,
587
- config: initialConfig,
588
- signatures: approve({
589
- config: initialConfig,
590
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
591
- signers: ownerKeys,
592
- }),
593
- }),
255
+ test('authorizes a multisig delegate and preserves the grant after delegate rotation', async () => {
256
+ const parent = await setup()
257
+ const delegate = await setup()
258
+ const grant = KeyAuthorization.from({
259
+ address: delegate.account,
260
+ account: parent.account,
261
+ chainId: BigInt(chain.id),
262
+ type: 'multisig',
263
+ })
264
+ const signed = KeyAuthorization.from(grant, {
265
+ signature: sign(parent, KeyAuthorization.getSignPayload(grant)),
266
+ })
267
+ const tx = { ...transaction(0n), keyAuthorization: signed }
268
+ const signature = SignatureEnvelope.from({
269
+ userAddress: parent.account,
270
+ version: 'v2',
271
+ inner: sign(
272
+ delegate,
273
+ TxEnvelopeTempo.getSignPayload(tx, { from: parent.account }),
274
+ ),
594
275
  })
595
-
596
- const bootstrap_receipt = (await client
597
- .request({
598
- method: 'eth_sendRawTransactionSync',
599
- params: [bootstrap_signed],
600
- })
601
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
602
- expect(bootstrap_receipt.status).toBe('success')
603
-
604
- // Multisig accounts authorize access keys through AccountKeychain.
605
- const authorizeKey = AbiFunction.from(
606
- 'function authorizeKey(address keyId, uint8 signatureType, (uint64 expiry, bool enforceLimits, (address token, uint256 amount, uint64 period)[] limits, bool allowAnyCalls, (address target, (bytes4 selector, address[] recipients)[] selectorRules)[] allowedCalls) config)',
276
+ expect((await submit(tx, signature)).status).toMatchInlineSnapshot(
277
+ '"success"',
607
278
  )
608
279
 
609
- const authorize = TxEnvelopeTempo.from({
610
- calls: [
611
- {
612
- to: '0xaaaaaaaa00000000000000000000000000000000',
613
- data: AbiFunction.encodeData(authorizeKey, [
614
- access.address,
615
- 0, // secp256k1
616
- {
617
- expiry: 18446744073709551615n, // u64 max (never expires)
618
- enforceLimits: false,
619
- limits: [],
620
- allowAnyCalls: true,
621
- allowedCalls: [],
622
- },
623
- ]),
624
- },
625
- ],
626
- chainId,
627
- feeToken: '0x20c0000000000000000000000000000000000001',
628
- nonce: 1n,
629
- gas: 5_000_000n,
630
- maxFeePerGas: Value.fromGwei('20'),
631
- maxPriorityFeePerGas: Value.fromGwei('10'),
632
- })
633
-
634
- const authorize_signed = TxEnvelopeTempo.serialize(authorize, {
635
- signature: SignatureEnvelope.from({
636
- account,
637
- config: initialConfig,
638
- signatures: approve({
639
- config: initialConfig,
640
- payload: TxEnvelopeTempo.getSignPayload(authorize),
641
- signers: ownerKeys,
642
- }),
643
- }),
644
- })
645
-
646
- const authorize_receipt = (await client
647
- .request({
648
- method: 'eth_sendRawTransactionSync',
649
- params: [authorize_signed],
650
- })
651
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
652
- expect(authorize_receipt.status).toBe('success')
653
- expect(authorize_receipt.from).toBe(account)
654
-
655
- const nonce = await getTransactionCount(client, {
656
- address: account,
657
- blockTag: 'pending',
658
- })
659
-
660
- const spend = TxEnvelopeTempo.from({
661
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
662
- chainId,
663
- feeToken: '0x20c0000000000000000000000000000000000001',
664
- nonce: BigInt(nonce),
665
- gas: 5_000_000n,
666
- maxFeePerGas: Value.fromGwei('20'),
667
- maxPriorityFeePerGas: Value.fromGwei('10'),
668
- })
669
-
670
- const spend_signature = Secp256k1.sign({
671
- payload: TxEnvelopeTempo.getSignPayload(spend, { from: account }),
672
- privateKey: access.privateKey,
673
- })
674
-
675
- const spend_signed = TxEnvelopeTempo.serialize(spend, {
676
- signature: SignatureEnvelope.from({
677
- userAddress: account,
678
- inner: SignatureEnvelope.from(spend_signature),
679
- type: 'keychain',
680
- }),
681
- })
682
-
683
- const spend_receipt = (await client
684
- .request({
685
- method: 'eth_sendRawTransactionSync',
686
- params: [spend_signed],
687
- })
688
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
689
- expect(spend_receipt.status).toBe('success')
690
- expect(spend_receipt.from).toBe(account)
691
-
692
- {
693
- const response = await client
694
- .request({
695
- method: 'eth_getTransactionByHash',
696
- params: [spend_receipt.transactionHash],
697
- })
698
- .then((tx) => Transaction.fromRpc(tx as any))
699
- if (!response) throw new Error()
700
- expect(response.from).toBe(account)
701
- expect(response.signature?.type).toBe('keychain')
702
- expect(
703
- (response.signature as SignatureEnvelope.Keychain | undefined)
704
- ?.userAddress,
705
- ).toBe(account)
280
+ const replacement = await setup()
281
+ const rotation = transaction(0n, [
282
+ {
283
+ to: precompile,
284
+ data: AbiFunction.encodeData(updateConfig, [
285
+ { ...delegate.config, salt: delegate.config.salt!, version: 0n },
286
+ 2,
287
+ replacement.config.owners,
288
+ ]),
289
+ },
290
+ ])
291
+ expect(
292
+ (
293
+ await submit(
294
+ rotation,
295
+ sign(delegate, TxEnvelopeTempo.getSignPayload(rotation)),
296
+ )
297
+ ).status,
298
+ ).toMatchInlineSnapshot('"success"')
299
+ const spend = transaction(1n)
300
+ const rotated = {
301
+ ...replacement,
302
+ account: delegate.account,
303
+ config: {
304
+ ...replacement.config,
305
+ salt: delegate.config.salt,
306
+ version: 1n,
307
+ },
706
308
  }
707
-
708
- const updateNonce = await getTransactionCount(client, {
709
- address: account,
710
- blockTag: 'pending',
711
- })
712
- const update = TxEnvelopeTempo.from({
713
- calls: [
714
- {
715
- to: '0xaacc000000000000000000000000000000000000',
716
- data: AbiFunction.encodeData(updateConfig, [
717
- initialConfig,
718
- initialConfig.threshold,
719
- initialConfig.owners,
720
- ]),
721
- },
722
- ],
723
- chainId,
724
- feeToken: '0x20c0000000000000000000000000000000000001',
725
- nonce: BigInt(updateNonce),
726
- gas: 5_000_000n,
727
- maxFeePerGas: Value.fromGwei('20'),
728
- maxPriorityFeePerGas: Value.fromGwei('10'),
729
- })
730
- const updateSignature = Secp256k1.sign({
731
- payload: TxEnvelopeTempo.getSignPayload(update, { from: account }),
732
- privateKey: access.privateKey,
733
- })
734
- const updateSigned = TxEnvelopeTempo.serialize(update, {
735
- signature: SignatureEnvelope.from({
736
- userAddress: account,
737
- inner: SignatureEnvelope.from(updateSignature),
738
- type: 'keychain',
739
- }),
740
- })
741
-
742
- const updateReceipt = (await client
743
- .request({
744
- method: 'eth_sendRawTransactionSync',
745
- params: [updateSigned],
746
- })
747
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
748
- expect(updateReceipt.status).toBe('reverted')
749
- })
750
-
751
- test('example: bootstrap and immediate access key use', async () => {
752
- const multisig = setup({ count: 2, threshold: 2 })
753
- const accessKey = createKey()
754
- await fundAddress(client, { address: multisig.account })
755
-
756
- const keyAuthorization = KeyAuthorization.from({
757
- account: multisig.account,
758
- address: accessKey.address,
759
- chainId: BigInt(chainId),
760
- isAdmin: false,
761
- type: 'secp256k1',
309
+ const payload = TxEnvelopeTempo.getSignPayload(spend, {
310
+ from: parent.account,
762
311
  })
763
- const keyAuthorizationSigned = KeyAuthorization.from(keyAuthorization, {
764
- signature: SignatureEnvelope.from({
765
- account: multisig.account,
766
- config: multisig.initialConfig,
767
- signatures: approve({
768
- config: multisig.initialConfig,
769
- payload: KeyAuthorization.getSignPayload(keyAuthorization),
770
- signers: multisig.ownerKeys,
312
+ await expect(
313
+ submit(
314
+ spend,
315
+ SignatureEnvelope.from({
316
+ userAddress: parent.account,
317
+ version: 'v2',
318
+ inner: sign(delegate, payload),
771
319
  }),
772
- }),
773
- })
774
- const transaction = TxEnvelopeTempo.from({
775
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
776
- chainId,
777
- feeToken: '0x20c0000000000000000000000000000000000001',
778
- keyAuthorization: keyAuthorizationSigned,
779
- nonce: 0n,
780
- gas: 5_000_000n,
781
- maxFeePerGas: Value.fromGwei('20'),
782
- maxPriorityFeePerGas: Value.fromGwei('10'),
320
+ ),
321
+ ).rejects.toThrow(/commitment mismatch/)
322
+ expect(
323
+ (
324
+ await submit(
325
+ spend,
326
+ SignatureEnvelope.from({
327
+ userAddress: parent.account,
328
+ version: 'v2',
329
+ inner: sign(rotated, payload),
330
+ }),
331
+ )
332
+ ).status,
333
+ ).toMatchInlineSnapshot('"success"')
334
+ const unauthorized = transaction(2n, [
335
+ {
336
+ to: precompile,
337
+ data: AbiFunction.encodeData(updateConfig, [
338
+ { ...parent.config, salt: parent.config.salt!, version: 0n },
339
+ 1,
340
+ parent.config.owners,
341
+ ]),
342
+ },
343
+ ])
344
+ const unauthorizedSignature = SignatureEnvelope.from({
345
+ userAddress: parent.account,
346
+ version: 'v2',
347
+ inner: sign(
348
+ rotated,
349
+ TxEnvelopeTempo.getSignPayload(unauthorized, { from: parent.account }),
350
+ ),
783
351
  })
784
- const receipt = await send(
785
- TxEnvelopeTempo.serialize(transaction, {
786
- signature: accessKeySignature({
787
- accessKey,
788
- account: multisig.account,
789
- transaction,
790
- }),
791
- }),
792
- )
793
- expect(receipt.status).toBe('success')
794
- expect(receipt.from).toBe(multisig.account)
352
+ expect(
353
+ (await submit(unauthorized, unauthorizedSignature)).status,
354
+ ).toMatchInlineSnapshot('"reverted"')
795
355
  })
796
356
 
797
- test('example: bootstrap and subsequent access key use', async () => {
798
- const multisig = setup({ count: 2, threshold: 2 })
799
- const accessKey = createKey()
800
- await fundAddress(client, { address: multisig.account })
801
-
802
- const keyAuthorization = KeyAuthorization.from({
803
- account: multisig.account,
804
- address: accessKey.address,
805
- chainId: BigInt(chainId),
806
- isAdmin: false,
357
+ test('rejects a grant signed by an individual owner instead of the account quorum', async () => {
358
+ const account = await setup()
359
+ const key = account.keys[0]!
360
+ const grant = KeyAuthorization.from({
361
+ address: key.address,
362
+ account: account.account,
363
+ chainId: BigInt(chain.id),
807
364
  type: 'secp256k1',
808
365
  })
809
- const keyAuthorizationSigned = KeyAuthorization.from(keyAuthorization, {
810
- signature: SignatureEnvelope.from({
811
- account: multisig.account,
812
- config: multisig.initialConfig,
813
- signatures: approve({
814
- config: multisig.initialConfig,
815
- payload: KeyAuthorization.getSignPayload(keyAuthorization),
816
- signers: multisig.ownerKeys,
817
- }),
818
- }),
819
- })
820
- const bootstrapTransaction = TxEnvelopeTempo.from({
821
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
822
- chainId,
823
- feeToken: '0x20c0000000000000000000000000000000000001',
824
- keyAuthorization: keyAuthorizationSigned,
825
- nonce: 0n,
826
- gas: 5_000_000n,
827
- maxFeePerGas: Value.fromGwei('20'),
828
- maxPriorityFeePerGas: Value.fromGwei('10'),
829
- })
830
- const bootstrapReceipt = await send(
831
- TxEnvelopeTempo.serialize(bootstrapTransaction, {
832
- signature: SignatureEnvelope.from({
833
- account: multisig.account,
834
- config: multisig.initialConfig,
835
- signatures: approve({
836
- config: multisig.initialConfig,
837
- payload: TxEnvelopeTempo.getSignPayload(bootstrapTransaction),
838
- signers: multisig.ownerKeys,
839
- }),
840
- }),
366
+ const signed = KeyAuthorization.from(grant, {
367
+ signature: Secp256k1.sign({
368
+ payload: KeyAuthorization.getSignPayload(grant),
369
+ privateKey: key.privateKey,
841
370
  }),
842
- )
843
- expect(bootstrapReceipt.status).toBe('success')
844
-
845
- const transaction = TxEnvelopeTempo.from({
846
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
847
- chainId,
848
- feeToken: '0x20c0000000000000000000000000000000000001',
849
- nonce: 1n,
850
- gas: 5_000_000n,
851
- maxFeePerGas: Value.fromGwei('20'),
852
- maxPriorityFeePerGas: Value.fromGwei('10'),
853
371
  })
854
- const receipt = await send(
855
- TxEnvelopeTempo.serialize(transaction, {
856
- signature: accessKeySignature({
857
- accessKey,
858
- account: multisig.account,
859
- transaction,
860
- }),
861
- }),
862
- )
863
- expect(receipt.status).toBe('success')
864
- expect(receipt.from).toBe(multisig.account)
372
+ const tx = { ...transaction(0n), keyAuthorization: signed }
373
+ await expect(
374
+ submit(tx, sign(account, TxEnvelopeTempo.getSignPayload(tx))),
375
+ ).rejects.toThrow()
865
376
  })
866
377
  })