ox 1.0.1 → 1.0.3
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.
- package/CHANGELOG.md +12 -0
- package/dist/tempo/KeyAuthorization.d.ts +22 -16
- package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/KeyAuthorization.js +31 -7
- package/dist/tempo/KeyAuthorization.js.map +1 -1
- package/dist/tempo/MultisigConfig.d.ts +8 -4
- package/dist/tempo/MultisigConfig.d.ts.map +1 -1
- package/dist/tempo/MultisigConfig.js +16 -4
- package/dist/tempo/MultisigConfig.js.map +1 -1
- package/dist/tempo/SignatureEnvelope.d.ts +31 -13
- package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/dist/tempo/SignatureEnvelope.js +172 -55
- package/dist/tempo/SignatureEnvelope.js.map +1 -1
- package/dist/zod/tempo/AuthorizationTempo.d.ts +70 -60
- package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
- package/dist/zod/tempo/KeyAuthorization.d.ts +0 -306
- package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
- package/dist/zod/tempo/KeyAuthorization.js +2 -2
- package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
- package/dist/zod/tempo/MultisigConfig.d.ts.map +1 -1
- package/dist/zod/tempo/MultisigConfig.js +5 -2
- package/dist/zod/tempo/MultisigConfig.js.map +1 -1
- package/dist/zod/tempo/RpcSchemaTempo.d.ts +28 -132
- package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
- package/dist/zod/tempo/SignatureEnvelope.d.ts +79 -18
- package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/dist/zod/tempo/SignatureEnvelope.js +39 -11
- package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
- package/dist/zod/tempo/Transaction.d.ts +84 -396
- package/dist/zod/tempo/Transaction.d.ts.map +1 -1
- package/dist/zod/tempo/TransactionRequest.d.ts +42 -234
- package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
- package/dist/zod/tempo/TxEnvelopeTempo.d.ts +0 -108
- package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/tempo/KeyAuthorization.test-d.ts +62 -0
- package/src/tempo/KeyAuthorization.test.ts +53 -0
- package/src/tempo/KeyAuthorization.ts +69 -27
- package/src/tempo/MultisigConfig.test.ts +34 -2
- package/src/tempo/MultisigConfig.ts +18 -4
- package/src/tempo/SignatureEnvelope.test-d.ts +97 -0
- package/src/tempo/SignatureEnvelope.test.ts +247 -36
- package/src/tempo/SignatureEnvelope.ts +271 -85
- package/src/tempo/e2e.test.ts +0 -416
- package/src/tempo/multisig.e2e.test.ts +582 -0
- package/src/version.ts +1 -1
- package/src/zod/tempo/KeyAuthorization.ts +2 -2
- package/src/zod/tempo/MultisigConfig.ts +13 -5
- package/src/zod/tempo/SignatureEnvelope.ts +81 -18
- package/src/zod/tempo/_test/KeyAuthorization.test.ts +25 -0
- package/src/zod/tempo/_test/MultisigConfig.test.ts +44 -0
- package/src/zod/tempo/_test/SignatureEnvelope.test-d.ts +9 -0
- package/src/zod/tempo/_test/SignatureEnvelope.test.ts +142 -6
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { AbiFunction, Address, Hex, Secp256k1, Value } from 'ox'
|
|
2
|
+
import { getTransactionCount } from 'viem/actions'
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
|
+
import { chain, client, fundAddress } from '../../test/tempo/config.js'
|
|
5
|
+
import { KeyAuthorization, MultisigConfig, SignatureEnvelope } from './index.js'
|
|
6
|
+
import * as Transaction from './Transaction.js'
|
|
7
|
+
import * as TransactionReceipt from './TransactionReceipt.js'
|
|
8
|
+
import * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
|
|
9
|
+
|
|
10
|
+
const chainId = chain.id
|
|
11
|
+
|
|
12
|
+
describe('behavior: multisig (TIP-1061)', () => {
|
|
13
|
+
function setup(parameters: { count: number; threshold: number }) {
|
|
14
|
+
const { count, threshold } = parameters
|
|
15
|
+
const ownerKeys = Array.from({ length: count }, () => {
|
|
16
|
+
const privateKey = Secp256k1.randomPrivateKey()
|
|
17
|
+
const address = Address.fromPublicKey(
|
|
18
|
+
Secp256k1.getPublicKey({ privateKey }),
|
|
19
|
+
)
|
|
20
|
+
return { address, privateKey } as const
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const genesisConfig = MultisigConfig.from({
|
|
24
|
+
salt: Hex.random(32),
|
|
25
|
+
threshold,
|
|
26
|
+
owners: ownerKeys.map((key) => ({
|
|
27
|
+
owner: key.address,
|
|
28
|
+
weight: 1,
|
|
29
|
+
})),
|
|
30
|
+
})
|
|
31
|
+
const account = MultisigConfig.getAddress(genesisConfig)
|
|
32
|
+
|
|
33
|
+
return { account, genesisConfig, ownerKeys } as const
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function approve(parameters: {
|
|
37
|
+
genesisConfig: MultisigConfig.Config
|
|
38
|
+
payload: Hex.Hex
|
|
39
|
+
signers: readonly { privateKey: Hex.Hex }[]
|
|
40
|
+
}) {
|
|
41
|
+
const { genesisConfig, payload, signers } = parameters
|
|
42
|
+
const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
|
|
43
|
+
const signatures = signers.map((signer) =>
|
|
44
|
+
SignatureEnvelope.from(
|
|
45
|
+
Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
// The node requires approvals ordered by recovered owner address.
|
|
49
|
+
return SignatureEnvelope.sortMultisigApprovals({
|
|
50
|
+
genesisConfig,
|
|
51
|
+
payload,
|
|
52
|
+
signatures,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('behavior: bootstrap + spend (2-of-3 secp256k1)', async () => {
|
|
57
|
+
const { account, genesisConfig, ownerKeys } = setup({
|
|
58
|
+
count: 3,
|
|
59
|
+
threshold: 2,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await fundAddress(client, { address: account })
|
|
63
|
+
|
|
64
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
65
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
66
|
+
chainId,
|
|
67
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
68
|
+
nonce: 0n,
|
|
69
|
+
gas: 5_000_000n,
|
|
70
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
71
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
|
|
75
|
+
signature: SignatureEnvelope.from({
|
|
76
|
+
genesisConfig,
|
|
77
|
+
init: true,
|
|
78
|
+
signatures: approve({
|
|
79
|
+
genesisConfig,
|
|
80
|
+
payload: TxEnvelopeTempo.getSignPayload(bootstrap),
|
|
81
|
+
signers: [ownerKeys[0]!, ownerKeys[1]!],
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const bootstrap_receipt = (await client
|
|
87
|
+
.request({
|
|
88
|
+
method: 'eth_sendRawTransactionSync',
|
|
89
|
+
params: [bootstrap_signed],
|
|
90
|
+
})
|
|
91
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
92
|
+
expect(bootstrap_receipt).toBeDefined()
|
|
93
|
+
expect(bootstrap_receipt.status).toBe('success')
|
|
94
|
+
expect(bootstrap_receipt.from).toBe(account)
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
const response = await client
|
|
98
|
+
.request({
|
|
99
|
+
method: 'eth_getTransactionByHash',
|
|
100
|
+
params: [bootstrap_receipt.transactionHash],
|
|
101
|
+
})
|
|
102
|
+
.then((tx) => Transaction.fromRpc(tx as any))
|
|
103
|
+
if (!response) throw new Error()
|
|
104
|
+
expect(response.from).toBe(account)
|
|
105
|
+
expect(response.signature?.type).toBe('multisig')
|
|
106
|
+
expect(
|
|
107
|
+
(response.signature as SignatureEnvelope.Multisig | undefined)?.init,
|
|
108
|
+
).toEqual(genesisConfig)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nonce = await getTransactionCount(client, {
|
|
112
|
+
address: account,
|
|
113
|
+
blockTag: 'pending',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const spend = TxEnvelopeTempo.from({
|
|
117
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
118
|
+
chainId,
|
|
119
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
120
|
+
nonce: BigInt(nonce),
|
|
121
|
+
gas: 5_000_000n,
|
|
122
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
123
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const spend_signed = TxEnvelopeTempo.serialize(spend, {
|
|
127
|
+
signature: SignatureEnvelope.from({
|
|
128
|
+
genesisConfig,
|
|
129
|
+
signatures: approve({
|
|
130
|
+
genesisConfig,
|
|
131
|
+
payload: TxEnvelopeTempo.getSignPayload(spend),
|
|
132
|
+
signers: [ownerKeys[1]!, ownerKeys[2]!],
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const spend_receipt = (await client
|
|
138
|
+
.request({
|
|
139
|
+
method: 'eth_sendRawTransactionSync',
|
|
140
|
+
params: [spend_signed],
|
|
141
|
+
})
|
|
142
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
143
|
+
expect(spend_receipt).toBeDefined()
|
|
144
|
+
expect(spend_receipt.status).toBe('success')
|
|
145
|
+
expect(spend_receipt.from).toBe(account)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('behavior: nested multisig owner', async () => {
|
|
149
|
+
const child = setup({ count: 1, threshold: 1 })
|
|
150
|
+
await fundAddress(client, { address: child.account })
|
|
151
|
+
|
|
152
|
+
const childBootstrap = TxEnvelopeTempo.from({
|
|
153
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
154
|
+
chainId,
|
|
155
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
156
|
+
nonce: 0n,
|
|
157
|
+
gas: 5_000_000n,
|
|
158
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
159
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
160
|
+
})
|
|
161
|
+
const childBootstrapSigned = TxEnvelopeTempo.serialize(childBootstrap, {
|
|
162
|
+
signature: SignatureEnvelope.from({
|
|
163
|
+
genesisConfig: child.genesisConfig,
|
|
164
|
+
init: true,
|
|
165
|
+
signatures: approve({
|
|
166
|
+
genesisConfig: child.genesisConfig,
|
|
167
|
+
payload: TxEnvelopeTempo.getSignPayload(childBootstrap),
|
|
168
|
+
signers: child.ownerKeys,
|
|
169
|
+
}),
|
|
170
|
+
}),
|
|
171
|
+
})
|
|
172
|
+
const childReceipt = (await client
|
|
173
|
+
.request({
|
|
174
|
+
method: 'eth_sendRawTransactionSync',
|
|
175
|
+
params: [childBootstrapSigned],
|
|
176
|
+
})
|
|
177
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
178
|
+
expect(childReceipt.status).toBe('success')
|
|
179
|
+
|
|
180
|
+
const genesisConfig = MultisigConfig.from({
|
|
181
|
+
salt: Hex.random(32),
|
|
182
|
+
threshold: 1,
|
|
183
|
+
owners: [{ owner: child.account, weight: 1 }],
|
|
184
|
+
})
|
|
185
|
+
const account = MultisigConfig.getAddress(genesisConfig)
|
|
186
|
+
await fundAddress(client, { address: account })
|
|
187
|
+
|
|
188
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
189
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
190
|
+
chainId,
|
|
191
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
192
|
+
nonce: 0n,
|
|
193
|
+
gas: 5_000_000n,
|
|
194
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
195
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
196
|
+
})
|
|
197
|
+
const digest = MultisigConfig.getSignPayload({
|
|
198
|
+
genesisConfig,
|
|
199
|
+
payload: TxEnvelopeTempo.getSignPayload(bootstrap),
|
|
200
|
+
})
|
|
201
|
+
const nested = SignatureEnvelope.from({
|
|
202
|
+
genesisConfig: child.genesisConfig,
|
|
203
|
+
signatures: approve({
|
|
204
|
+
genesisConfig: child.genesisConfig,
|
|
205
|
+
payload: digest,
|
|
206
|
+
signers: child.ownerKeys,
|
|
207
|
+
}),
|
|
208
|
+
})
|
|
209
|
+
const bootstrapSigned = TxEnvelopeTempo.serialize(bootstrap, {
|
|
210
|
+
signature: SignatureEnvelope.from({
|
|
211
|
+
genesisConfig,
|
|
212
|
+
init: true,
|
|
213
|
+
signatures: [nested],
|
|
214
|
+
}),
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const receipt = (await client
|
|
218
|
+
.request({
|
|
219
|
+
method: 'eth_sendRawTransactionSync',
|
|
220
|
+
params: [bootstrapSigned],
|
|
221
|
+
})
|
|
222
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
223
|
+
expect(receipt.status).toBe('success')
|
|
224
|
+
expect(receipt.from).toBe(account)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('behavior: rejects below-threshold approvals', async () => {
|
|
228
|
+
const { account, genesisConfig, ownerKeys } = setup({
|
|
229
|
+
count: 3,
|
|
230
|
+
threshold: 2,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
await fundAddress(client, { address: account })
|
|
234
|
+
|
|
235
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
236
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
237
|
+
chainId,
|
|
238
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
239
|
+
nonce: 0n,
|
|
240
|
+
gas: 5_000_000n,
|
|
241
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
242
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
243
|
+
})
|
|
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]!],
|
|
253
|
+
}),
|
|
254
|
+
}),
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
await expect(
|
|
258
|
+
client.request({
|
|
259
|
+
method: 'eth_sendRawTransactionSync',
|
|
260
|
+
params: [serialized_signed],
|
|
261
|
+
}),
|
|
262
|
+
).rejects.toThrow()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('behavior: keychain access key (authorize, spend, reject config update)', async () => {
|
|
266
|
+
const { account, genesisConfig, ownerKeys } = setup({
|
|
267
|
+
count: 2,
|
|
268
|
+
threshold: 2,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
await fundAddress(client, { address: account })
|
|
272
|
+
|
|
273
|
+
const access = (() => {
|
|
274
|
+
const privateKey = Secp256k1.randomPrivateKey()
|
|
275
|
+
const address = Address.fromPublicKey(
|
|
276
|
+
Secp256k1.getPublicKey({ privateKey }),
|
|
277
|
+
)
|
|
278
|
+
return { address, privateKey } as const
|
|
279
|
+
})()
|
|
280
|
+
|
|
281
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
282
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
283
|
+
chainId,
|
|
284
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
285
|
+
nonce: 0n,
|
|
286
|
+
gas: 5_000_000n,
|
|
287
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
288
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
|
|
292
|
+
signature: SignatureEnvelope.from({
|
|
293
|
+
genesisConfig,
|
|
294
|
+
init: true,
|
|
295
|
+
signatures: approve({
|
|
296
|
+
genesisConfig,
|
|
297
|
+
payload: TxEnvelopeTempo.getSignPayload(bootstrap),
|
|
298
|
+
signers: ownerKeys,
|
|
299
|
+
}),
|
|
300
|
+
}),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const bootstrap_receipt = (await client
|
|
304
|
+
.request({
|
|
305
|
+
method: 'eth_sendRawTransactionSync',
|
|
306
|
+
params: [bootstrap_signed],
|
|
307
|
+
})
|
|
308
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
309
|
+
expect(bootstrap_receipt.status).toBe('success')
|
|
310
|
+
|
|
311
|
+
// Multisig accounts authorize access keys through AccountKeychain.
|
|
312
|
+
const authorizeKey = AbiFunction.from(
|
|
313
|
+
'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)',
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const authorize = TxEnvelopeTempo.from({
|
|
317
|
+
calls: [
|
|
318
|
+
{
|
|
319
|
+
to: '0xaaaaaaaa00000000000000000000000000000000',
|
|
320
|
+
data: AbiFunction.encodeData(authorizeKey, [
|
|
321
|
+
access.address,
|
|
322
|
+
0, // secp256k1
|
|
323
|
+
{
|
|
324
|
+
expiry: 18446744073709551615n, // u64 max (never expires)
|
|
325
|
+
enforceLimits: false,
|
|
326
|
+
limits: [],
|
|
327
|
+
allowAnyCalls: true,
|
|
328
|
+
allowedCalls: [],
|
|
329
|
+
},
|
|
330
|
+
]),
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
chainId,
|
|
334
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
335
|
+
nonce: 1n,
|
|
336
|
+
gas: 5_000_000n,
|
|
337
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
338
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const authorize_signed = TxEnvelopeTempo.serialize(authorize, {
|
|
342
|
+
signature: SignatureEnvelope.from({
|
|
343
|
+
genesisConfig,
|
|
344
|
+
signatures: approve({
|
|
345
|
+
genesisConfig,
|
|
346
|
+
payload: TxEnvelopeTempo.getSignPayload(authorize),
|
|
347
|
+
signers: ownerKeys,
|
|
348
|
+
}),
|
|
349
|
+
}),
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
const authorize_receipt = (await client
|
|
353
|
+
.request({
|
|
354
|
+
method: 'eth_sendRawTransactionSync',
|
|
355
|
+
params: [authorize_signed],
|
|
356
|
+
})
|
|
357
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
358
|
+
expect(authorize_receipt.status).toBe('success')
|
|
359
|
+
expect(authorize_receipt.from).toBe(account)
|
|
360
|
+
|
|
361
|
+
const nonce = await getTransactionCount(client, {
|
|
362
|
+
address: account,
|
|
363
|
+
blockTag: 'pending',
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const spend = TxEnvelopeTempo.from({
|
|
367
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
368
|
+
chainId,
|
|
369
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
370
|
+
nonce: BigInt(nonce),
|
|
371
|
+
gas: 5_000_000n,
|
|
372
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
373
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const spend_signature = Secp256k1.sign({
|
|
377
|
+
payload: TxEnvelopeTempo.getSignPayload(spend, { from: account }),
|
|
378
|
+
privateKey: access.privateKey,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const spend_signed = TxEnvelopeTempo.serialize(spend, {
|
|
382
|
+
signature: SignatureEnvelope.from({
|
|
383
|
+
userAddress: account,
|
|
384
|
+
inner: SignatureEnvelope.from(spend_signature),
|
|
385
|
+
type: 'keychain',
|
|
386
|
+
}),
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const spend_receipt = (await client
|
|
390
|
+
.request({
|
|
391
|
+
method: 'eth_sendRawTransactionSync',
|
|
392
|
+
params: [spend_signed],
|
|
393
|
+
})
|
|
394
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
395
|
+
expect(spend_receipt.status).toBe('success')
|
|
396
|
+
expect(spend_receipt.from).toBe(account)
|
|
397
|
+
|
|
398
|
+
{
|
|
399
|
+
const response = await client
|
|
400
|
+
.request({
|
|
401
|
+
method: 'eth_getTransactionByHash',
|
|
402
|
+
params: [spend_receipt.transactionHash],
|
|
403
|
+
})
|
|
404
|
+
.then((tx) => Transaction.fromRpc(tx as any))
|
|
405
|
+
if (!response) throw new Error()
|
|
406
|
+
expect(response.from).toBe(account)
|
|
407
|
+
expect(response.signature?.type).toBe('keychain')
|
|
408
|
+
expect(
|
|
409
|
+
(response.signature as SignatureEnvelope.Keychain | undefined)
|
|
410
|
+
?.userAddress,
|
|
411
|
+
).toBe(account)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const updateMultisigConfig = AbiFunction.from(
|
|
415
|
+
'function updateMultisigConfig(uint8 threshold, (address owner, uint8 weight)[] owners)',
|
|
416
|
+
)
|
|
417
|
+
const updateNonce = await getTransactionCount(client, {
|
|
418
|
+
address: account,
|
|
419
|
+
blockTag: 'pending',
|
|
420
|
+
})
|
|
421
|
+
const update = TxEnvelopeTempo.from({
|
|
422
|
+
calls: [
|
|
423
|
+
{
|
|
424
|
+
to: '0xaacc000000000000000000000000000000000000',
|
|
425
|
+
data: AbiFunction.encodeData(updateMultisigConfig, [
|
|
426
|
+
genesisConfig.threshold,
|
|
427
|
+
genesisConfig.owners,
|
|
428
|
+
]),
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
chainId,
|
|
432
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
433
|
+
nonce: BigInt(updateNonce),
|
|
434
|
+
gas: 5_000_000n,
|
|
435
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
436
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
437
|
+
})
|
|
438
|
+
const updateSignature = Secp256k1.sign({
|
|
439
|
+
payload: TxEnvelopeTempo.getSignPayload(update, { from: account }),
|
|
440
|
+
privateKey: access.privateKey,
|
|
441
|
+
})
|
|
442
|
+
const updateSigned = TxEnvelopeTempo.serialize(update, {
|
|
443
|
+
signature: SignatureEnvelope.from({
|
|
444
|
+
userAddress: account,
|
|
445
|
+
inner: SignatureEnvelope.from(updateSignature),
|
|
446
|
+
type: 'keychain',
|
|
447
|
+
}),
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const updateReceipt = (await client
|
|
451
|
+
.request({
|
|
452
|
+
method: 'eth_sendRawTransactionSync',
|
|
453
|
+
params: [updateSigned],
|
|
454
|
+
})
|
|
455
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
456
|
+
expect(updateReceipt.status).toBe('reverted')
|
|
457
|
+
})
|
|
458
|
+
|
|
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
|
+
})
|
|
464
|
+
|
|
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,
|
|
477
|
+
chainId: BigInt(chainId),
|
|
478
|
+
type: 'secp256k1',
|
|
479
|
+
})
|
|
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
|
+
),
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
490
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
491
|
+
chainId,
|
|
492
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
493
|
+
keyAuthorization: keyAuth_signed,
|
|
494
|
+
nonce: 0n,
|
|
495
|
+
gas: 5_000_000n,
|
|
496
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
497
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
498
|
+
})
|
|
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,
|
|
508
|
+
}),
|
|
509
|
+
}),
|
|
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
|
+
)
|
|
520
|
+
})
|
|
521
|
+
|
|
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
|
+
})()
|
|
537
|
+
|
|
538
|
+
const keyAuth = KeyAuthorization.from({
|
|
539
|
+
address: access.address,
|
|
540
|
+
chainId: BigInt(chainId),
|
|
541
|
+
type: 'secp256k1',
|
|
542
|
+
})
|
|
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
|
+
),
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
553
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
554
|
+
chainId,
|
|
555
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
556
|
+
keyAuthorization: keyAuth_signed,
|
|
557
|
+
nonce: 0n,
|
|
558
|
+
gas: 5_000_000n,
|
|
559
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
560
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
561
|
+
})
|
|
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',
|
|
572
|
+
}),
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
await expect(
|
|
576
|
+
client.request({
|
|
577
|
+
method: 'eth_sendRawTransactionSync',
|
|
578
|
+
params: [serialized_signed],
|
|
579
|
+
}),
|
|
580
|
+
).rejects.toThrow('admin-signed key authorization account mismatch')
|
|
581
|
+
})
|
|
582
|
+
})
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export const version = '1.0.
|
|
2
|
+
export const version = '1.0.3'
|
|
@@ -38,7 +38,7 @@ export const Rpc = z.object({
|
|
|
38
38
|
keyId: z_Address.Address,
|
|
39
39
|
keyType: z_SignatureEnvelope.Type,
|
|
40
40
|
limits: z.optional(z.nullable(z.readonly(z.array(RpcTokenLimit)))),
|
|
41
|
-
signature: z_SignatureEnvelope.
|
|
41
|
+
signature: z_SignatureEnvelope.PrimitiveRpc,
|
|
42
42
|
witness: z.optional(z.nullable(z_Hex.Hex)),
|
|
43
43
|
})
|
|
44
44
|
|
|
@@ -69,7 +69,7 @@ const domainShape = {
|
|
|
69
69
|
expiry: z.optional(z.number()),
|
|
70
70
|
limits: z.optional(z.readonly(z.array(TokenLimit))),
|
|
71
71
|
scopes: z.optional(z.readonly(z.array(Scope))),
|
|
72
|
-
signature: z_SignatureEnvelope.
|
|
72
|
+
signature: z_SignatureEnvelope.Primitive,
|
|
73
73
|
type: z_SignatureEnvelope.Type,
|
|
74
74
|
witness: z.optional(z_Hex.Hex),
|
|
75
75
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable jsdoc-js/require-jsdoc, jsdoc-js/require-description, jsdoc-js/require-example */
|
|
2
|
+
import * as core_MultisigConfig from '../../tempo/MultisigConfig.js'
|
|
2
3
|
import * as z_Address from '../Address.js'
|
|
3
4
|
import * as z_Hex from '../Hex.js'
|
|
4
5
|
import * as z from 'zod/mini'
|
|
@@ -10,8 +11,15 @@ export const Owner = z.object({
|
|
|
10
11
|
})
|
|
11
12
|
|
|
12
13
|
/** Native multisig configuration schema. */
|
|
13
|
-
export const Config = z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export const Config = z
|
|
15
|
+
.object({
|
|
16
|
+
salt: z.optional(z_Hex.Hex),
|
|
17
|
+
threshold: z.number(),
|
|
18
|
+
owners: z.readonly(z.array(Owner)),
|
|
19
|
+
})
|
|
20
|
+
.check(
|
|
21
|
+
z.refine(
|
|
22
|
+
(value) => core_MultisigConfig.validate(value),
|
|
23
|
+
'expected valid native multisig configuration',
|
|
24
|
+
),
|
|
25
|
+
)
|