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
@@ -1,4 +1,4 @@
1
- import { Bytes, Hex, P256 } from 'ox'
1
+ import { Bytes, Engine, Hex, Mnemonic, P256 } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
  import { accounts } from '../../../test/constants/accounts.js'
4
4
 
@@ -145,6 +145,100 @@ describe('createKeyPair', () => {
145
145
  })
146
146
  })
147
147
 
148
+ describe('fromMnemonic', () => {
149
+ const mnemonic = 'test test test test test test test test test test test junk'
150
+
151
+ test('default', () => {
152
+ const privateKey = P256.fromMnemonic(mnemonic)
153
+
154
+ expect(privateKey).toBe(P256.fromSeed(Mnemonic.toSeed(mnemonic)))
155
+ expect(privateKey).toMatchInlineSnapshot(
156
+ `"0x882f5b02a84c96bceeebf9c868bec73041d51b041ae05380e66a41508648ebc3"`,
157
+ )
158
+ })
159
+
160
+ test('options: passphrase', () => {
161
+ expect(
162
+ P256.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
163
+ ).toMatchInlineSnapshot(
164
+ `"0x364ff539f37ce477d49a35476c9ad32a2a3fed1d1237eaee7abdfd07655ad9f5"`,
165
+ )
166
+ })
167
+
168
+ test('options: as', () => {
169
+ const privateKey = P256.fromMnemonic(mnemonic, { as: 'Bytes' })
170
+
171
+ expect(privateKey).toBeInstanceOf(Uint8Array)
172
+ expect(privateKey).toHaveLength(32)
173
+ })
174
+ })
175
+
176
+ describe('fromSeed', () => {
177
+ const seed =
178
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
179
+
180
+ test('vector', () => {
181
+ expect(P256.fromSeed(seed)).toMatchInlineSnapshot(
182
+ `"0x724009e76eb27e34eb77b93db354b9015ecd4296deb57561f5214bab68b94a6f"`,
183
+ )
184
+ })
185
+
186
+ test('value: Bytes', () => {
187
+ expect(P256.fromSeed(Bytes.fromHex(seed))).toMatchInlineSnapshot(
188
+ `"0x724009e76eb27e34eb77b93db354b9015ecd4296deb57561f5214bab68b94a6f"`,
189
+ )
190
+ })
191
+
192
+ test('options: as', () => {
193
+ expect(P256.fromSeed(seed, { as: 'Bytes' })).toEqual(
194
+ Bytes.fromHex(
195
+ '0x724009e76eb27e34eb77b93db354b9015ecd4296deb57561f5214bab68b94a6f',
196
+ ),
197
+ )
198
+ })
199
+
200
+ test('behavior: output is a valid P256 private key', () => {
201
+ const privateKey = P256.fromSeed(new Uint8Array(64), { as: 'Bytes' })
202
+
203
+ expect(P256.noble.utils.isValidSecretKey(privateKey)).toBe(true)
204
+ })
205
+
206
+ test('behavior: skips invalid scalar candidates', () => {
207
+ const messages: Hex.Hex[] = []
208
+ const privateKey = Engine.with(
209
+ {
210
+ Hash: {
211
+ hmacSha256: (_key, message) => {
212
+ messages.push(Hex.fromBytes(message))
213
+ if (messages.length === 1) return new Uint8Array(32)
214
+ return Bytes.fromHex(
215
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
216
+ )
217
+ },
218
+ },
219
+ },
220
+ () => P256.fromSeed(new Uint8Array(32)),
221
+ )
222
+
223
+ expect(privateKey).toBe(
224
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
225
+ )
226
+ expect(messages).toMatchInlineSnapshot(`
227
+ [
228
+ "0x6f782e703235362e66726f6d536565642e763100000000",
229
+ "0x6f782e703235362e66726f6d536565642e763100000001",
230
+ ]
231
+ `)
232
+ })
233
+
234
+ test('error: seed is too short', () => {
235
+ expect(() => P256.fromSeed(new Uint8Array(31)))
236
+ .toThrowErrorMatchingInlineSnapshot(`
237
+ [P256.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.]
238
+ `)
239
+ })
240
+ })
241
+
148
242
  describe('getSharedSecret', () => {
149
243
  test('default', () => {
150
244
  const { privateKey: privateKeyA, publicKey: publicKeyA } =
@@ -546,12 +640,15 @@ test('exports', () => {
546
640
  [
547
641
  "noble",
548
642
  "createKeyPair",
643
+ "fromMnemonic",
644
+ "fromSeed",
549
645
  "getPublicKey",
550
646
  "getSharedSecret",
551
647
  "randomPrivateKey",
552
648
  "recoverPublicKey",
553
649
  "sign",
554
650
  "verify",
651
+ "InvalidSeedSizeError",
555
652
  ]
556
653
  `)
557
654
  })
@@ -12,3 +12,30 @@ test('fromPrf', () => {
12
12
  Secp256k1.fromPrf(prf, { as: 'Bytes' }),
13
13
  ).toEqualTypeOf<Bytes.Bytes>()
14
14
  })
15
+
16
+ test('fromMnemonic', () => {
17
+ const mnemonic = 'test test test test test test test test test test test junk'
18
+
19
+ expectTypeOf(Secp256k1.fromMnemonic(mnemonic)).toEqualTypeOf<Hex.Hex>()
20
+ expectTypeOf(
21
+ Secp256k1.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
22
+ ).toEqualTypeOf<Hex.Hex>()
23
+ expectTypeOf(
24
+ Secp256k1.fromMnemonic(mnemonic, { as: 'Bytes' }),
25
+ ).toEqualTypeOf<Bytes.Bytes>()
26
+ expectTypeOf(
27
+ Secp256k1.fromMnemonic(mnemonic, { path: "m/44'/60'/0'/0/1" }),
28
+ ).toEqualTypeOf<Hex.Hex>()
29
+ })
30
+
31
+ test('fromSeed', () => {
32
+ const seed =
33
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
34
+
35
+ expectTypeOf(Secp256k1.fromSeed(seed)).toEqualTypeOf<Hex.Hex>()
36
+ expectTypeOf(Secp256k1.fromSeed(new Uint8Array(32))).toEqualTypeOf<Hex.Hex>()
37
+ expectTypeOf(Secp256k1.fromSeed(seed, { as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
38
+ expectTypeOf(
39
+ Secp256k1.fromSeed(seed, { as: 'Bytes' }),
40
+ ).toEqualTypeOf<Bytes.Bytes>()
41
+ })
@@ -1,4 +1,4 @@
1
- import { Address, Bytes, Engine, Hex, PublicKey, Secp256k1 } from 'ox'
1
+ import { Address, Bytes, Engine, Hex, Mnemonic, PublicKey, Secp256k1 } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
  import { accounts } from '../../../test/constants/accounts.js'
4
4
 
@@ -211,6 +211,114 @@ describe('fromPrf', () => {
211
211
  })
212
212
  })
213
213
 
214
+ describe('fromMnemonic', () => {
215
+ const mnemonic = 'test test test test test test test test test test test junk'
216
+
217
+ test('default', () => {
218
+ const privateKey = Secp256k1.fromMnemonic(mnemonic)
219
+
220
+ expect(privateKey).toBe(Mnemonic.toPrivateKey(mnemonic, { as: 'Hex' }))
221
+ expect(privateKey).toMatchInlineSnapshot(
222
+ `"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"`,
223
+ )
224
+ })
225
+
226
+ test('options: passphrase', () => {
227
+ expect(
228
+ Secp256k1.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
229
+ ).toMatchInlineSnapshot(
230
+ `"0x0bef893b1cc27e9ce726d5f12f75d61a07d4df87c02106083463cd712ac5c478"`,
231
+ )
232
+ })
233
+
234
+ test('options: as', () => {
235
+ const privateKey = Secp256k1.fromMnemonic(mnemonic, { as: 'Bytes' })
236
+
237
+ expect(privateKey).toBeInstanceOf(Uint8Array)
238
+ expect(privateKey).toHaveLength(32)
239
+ })
240
+
241
+ test('options: path', () => {
242
+ const path = "m/44'/60'/0'/0/1"
243
+
244
+ expect(Secp256k1.fromMnemonic(mnemonic, { path })).toBe(
245
+ Mnemonic.toPrivateKey(mnemonic, { as: 'Hex', path }),
246
+ )
247
+ })
248
+ })
249
+
250
+ describe('fromSeed', () => {
251
+ const seed =
252
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
253
+
254
+ test('vector', () => {
255
+ expect(Secp256k1.fromSeed(seed)).toMatchInlineSnapshot(
256
+ `"0x5f7dcbba41adaafa378861d78b144f7b2827e79fa994a341628f5470d5bfbdd4"`,
257
+ )
258
+ })
259
+
260
+ test('value: Bytes', () => {
261
+ expect(Secp256k1.fromSeed(Bytes.fromHex(seed))).toMatchInlineSnapshot(
262
+ `"0x5f7dcbba41adaafa378861d78b144f7b2827e79fa994a341628f5470d5bfbdd4"`,
263
+ )
264
+ })
265
+
266
+ test('options: as', () => {
267
+ expect(Secp256k1.fromSeed(seed, { as: 'Bytes' })).toEqual(
268
+ Bytes.fromHex(
269
+ '0x5f7dcbba41adaafa378861d78b144f7b2827e79fa994a341628f5470d5bfbdd4',
270
+ ),
271
+ )
272
+ })
273
+
274
+ test('behavior: accepts seeds longer than 32 bytes', () => {
275
+ const privateKey = Secp256k1.fromSeed(new Uint8Array(64), { as: 'Bytes' })
276
+
277
+ expect(Secp256k1.noble.utils.isValidSecretKey(privateKey)).toBe(true)
278
+ })
279
+
280
+ test('behavior: domain is distinct from fromPrf', () => {
281
+ expect(Secp256k1.fromSeed(seed)).not.toBe(Secp256k1.fromPrf(seed))
282
+ })
283
+
284
+ test('behavior: skips invalid scalar candidates', () => {
285
+ const invalid = new Uint8Array(32).fill(0xff)
286
+ const messages: Hex.Hex[] = []
287
+ const privateKey = Engine.with(
288
+ {
289
+ Hash: {
290
+ hmacSha256: (_key, message) => {
291
+ messages.push(Hex.fromBytes(message))
292
+ if (messages.length === 1) return invalid
293
+ return Bytes.fromHex(
294
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
295
+ )
296
+ },
297
+ },
298
+ },
299
+ () => Secp256k1.fromSeed(new Uint8Array(32)),
300
+ )
301
+
302
+ expect(privateKey).toBe(
303
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
304
+ )
305
+ expect(invalid).toEqual(new Uint8Array(32))
306
+ expect(messages).toMatchInlineSnapshot(`
307
+ [
308
+ "0x6f782e736563703235366b312e66726f6d536565642e763100000000",
309
+ "0x6f782e736563703235366b312e66726f6d536565642e763100000001",
310
+ ]
311
+ `)
312
+ })
313
+
314
+ test('error: seed is too short', () => {
315
+ expect(() => Secp256k1.fromSeed(new Uint8Array(31)))
316
+ .toThrowErrorMatchingInlineSnapshot(`
317
+ [Secp256k1.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.]
318
+ `)
319
+ })
320
+ })
321
+
214
322
  describe('getSharedSecret', () => {
215
323
  test('default', () => {
216
324
  const privateKeyA = accounts[0].privateKey
@@ -572,6 +680,8 @@ test('exports', () => {
572
680
  "noble",
573
681
  "createKeyPair",
574
682
  "fromPrf",
683
+ "fromMnemonic",
684
+ "fromSeed",
575
685
  "getPublicKey",
576
686
  "getSharedSecret",
577
687
  "randomPrivateKey",
@@ -580,6 +690,7 @@ test('exports', () => {
580
690
  "sign",
581
691
  "verify",
582
692
  "InvalidPrfSizeError",
693
+ "InvalidSeedSizeError",
583
694
  ]
584
695
  `)
585
696
  })
@@ -0,0 +1,34 @@
1
+ import * as Bytes from '../Bytes.js'
2
+ import type * as Errors from '../Errors.js'
3
+ import * as Hash from '../Hash.js'
4
+
5
+ /** @internal */
6
+ export function derive(
7
+ seed: Bytes.Bytes,
8
+ domain: Bytes.Bytes,
9
+ options: derive.Options = {},
10
+ ): Bytes.Bytes {
11
+ const { validate } = options
12
+ for (let counter = 0; ; counter++) {
13
+ const key = Hash.hmac256(
14
+ seed,
15
+ Bytes.concat(domain, Bytes.fromNumber(counter, { size: 4 })),
16
+ { as: 'Bytes' },
17
+ )
18
+ if (!validate || validate(key)) return key
19
+ key.fill(0)
20
+ }
21
+ }
22
+
23
+ /** @internal */
24
+ export declare namespace derive {
25
+ type Options = {
26
+ validate?: ((key: Bytes.Bytes) => boolean) | undefined
27
+ }
28
+
29
+ type ErrorType =
30
+ | Bytes.concat.ErrorType
31
+ | Bytes.fromNumber.ErrorType
32
+ | Hash.hmac256.ErrorType
33
+ | Errors.GlobalErrorType
34
+ }
@@ -23,6 +23,12 @@ const multisig = {
23
23
  type: 'multisig',
24
24
  } as const satisfies SignatureEnvelope.Multisig
25
25
 
26
+ const keychain = {
27
+ inner: signature,
28
+ type: 'keychain',
29
+ userAddress: authorization.address,
30
+ } as const satisfies SignatureEnvelope.Keychain
31
+
26
32
  test('accepts primitive signatures', () => {
27
33
  const signed = KeyAuthorization.from(authorization, { signature })
28
34
 
@@ -31,11 +37,12 @@ test('accepts primitive signatures', () => {
31
37
  >()
32
38
  })
33
39
 
34
- test('rejects multisig signatures', () => {
35
- KeyAuthorization.from(authorization, {
36
- // @ts-expect-error Key authorizations accept only primitive signatures.
37
- signature: multisig,
38
- })
40
+ test('accepts multisig signatures', () => {
41
+ const signed = KeyAuthorization.from(authorization, { signature: multisig })
42
+
43
+ expectTypeOf(signed.signature).toMatchTypeOf<
44
+ KeyAuthorization.Signed['signature']
45
+ >()
39
46
 
40
47
  const multisigRpc = {
41
48
  account: multisig.account,
@@ -54,9 +61,37 @@ test('rejects multisig signatures', () => {
54
61
  expiry: null,
55
62
  keyId: authorization.address,
56
63
  keyType: authorization.type,
57
- // @ts-expect-error Key authorizations accept only primitive RPC signatures.
58
64
  signature: multisigRpc,
59
65
  }
60
66
 
61
67
  expectTypeOf(rpc).toEqualTypeOf<KeyAuthorization.Rpc>()
62
68
  })
69
+
70
+ test('rejects keychain signatures', () => {
71
+ KeyAuthorization.from(authorization, {
72
+ // @ts-expect-error Key authorizations do not accept keychain signatures.
73
+ signature: keychain,
74
+ })
75
+
76
+ const keychainRpc = {
77
+ signature: {
78
+ r: '0x01',
79
+ s: '0x02',
80
+ type: 'secp256k1',
81
+ yParity: '0x0',
82
+ },
83
+ type: 'keychain',
84
+ userAddress: authorization.address,
85
+ } as const satisfies SignatureEnvelope.KeychainRpc
86
+
87
+ const rpc: KeyAuthorization.Rpc = {
88
+ chainId: '0x1',
89
+ expiry: null,
90
+ keyId: authorization.address,
91
+ keyType: authorization.type,
92
+ // @ts-expect-error Key authorizations do not accept keychain RPC signatures.
93
+ signature: keychainRpc,
94
+ }
95
+
96
+ expectTypeOf(rpc).toEqualTypeOf<KeyAuthorization.Rpc>()
97
+ })
@@ -53,11 +53,17 @@ const signature_webauthn = SignatureEnvelope.from({
53
53
  },
54
54
  })
55
55
 
56
- const signature_multisig = SignatureEnvelope.from({
56
+ const signature_multisig = {
57
57
  account: address,
58
58
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
59
59
  type: 'multisig',
60
- })
60
+ } as const satisfies SignatureEnvelope.Multisig
61
+
62
+ const signature_keychain = {
63
+ inner: SignatureEnvelope.from(signature_secp256k1),
64
+ type: 'keychain',
65
+ userAddress: address,
66
+ } as const satisfies SignatureEnvelope.Keychain
61
67
 
62
68
  describe('from', () => {
63
69
  test('default', () => {
@@ -423,7 +429,34 @@ describe('from', () => {
423
429
  `)
424
430
  })
425
431
 
426
- test('rejects a multisig signature', () => {
432
+ test('with signature (multisig)', () => {
433
+ const authorization = KeyAuthorization.from({
434
+ address,
435
+ chainId: 1n,
436
+ type: 'secp256k1',
437
+ })
438
+
439
+ const signed = KeyAuthorization.from(authorization, {
440
+ signature: signature_multisig,
441
+ })
442
+ expect(signed.signature).toEqual(signature_multisig)
443
+ expect(
444
+ KeyAuthorization.from(authorization, {
445
+ signature: SignatureEnvelope.serialize(signature_multisig),
446
+ }).signature,
447
+ ).toEqual(signature_multisig)
448
+ expect(
449
+ KeyAuthorization.from({
450
+ ...authorization,
451
+ signature: signature_multisig,
452
+ }).signature,
453
+ ).toEqual(signature_multisig)
454
+ expect(
455
+ KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)),
456
+ ).toEqual(signed)
457
+ })
458
+
459
+ test('rejects a keychain signature', () => {
427
460
  const authorization = KeyAuthorization.from({
428
461
  address,
429
462
  chainId: 1n,
@@ -432,43 +465,55 @@ describe('from', () => {
432
465
 
433
466
  expect(() =>
434
467
  KeyAuthorization.from(authorization, {
435
- signature: signature_multisig as never,
468
+ signature: signature_keychain as never,
436
469
  }),
437
470
  ).toThrowErrorMatchingInlineSnapshot(
438
- `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
471
+ `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`,
439
472
  )
440
473
 
441
474
  expect(() =>
442
475
  KeyAuthorization.from(authorization, {
443
- signature: SignatureEnvelope.serialize(signature_multisig),
476
+ signature: SignatureEnvelope.serialize(signature_keychain),
444
477
  }),
445
478
  ).toThrowErrorMatchingInlineSnapshot(
446
- `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
479
+ `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`,
447
480
  )
448
481
 
449
482
  expect(() =>
450
483
  KeyAuthorization.from({
451
484
  ...authorization,
452
- signature: signature_multisig,
485
+ signature: signature_keychain,
453
486
  } as never),
454
487
  ).toThrowErrorMatchingInlineSnapshot(
455
- `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
488
+ `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`,
456
489
  )
457
490
  })
458
491
  })
459
492
 
460
493
  describe('fromRpc', () => {
461
- test('rejects a multisig signature', () => {
494
+ test('multisig', () => {
495
+ const authorization = KeyAuthorization.fromRpc({
496
+ chainId: '0x1',
497
+ expiry: null,
498
+ keyId: address,
499
+ keyType: 'secp256k1',
500
+ signature: SignatureEnvelope.toRpc(signature_multisig),
501
+ })
502
+
503
+ expect(authorization.signature).toEqual(signature_multisig)
504
+ })
505
+
506
+ test('rejects a keychain signature', () => {
462
507
  expect(() =>
463
508
  KeyAuthorization.fromRpc({
464
509
  chainId: '0x1',
465
510
  expiry: null,
466
511
  keyId: address,
467
512
  keyType: 'secp256k1',
468
- signature: SignatureEnvelope.toRpc(signature_multisig),
513
+ signature: SignatureEnvelope.toRpc(signature_keychain),
469
514
  } as never),
470
515
  ).toThrowErrorMatchingInlineSnapshot(
471
- `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`multisig\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.]`,
516
+ `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`,
472
517
  )
473
518
  })
474
519
 
@@ -2401,12 +2446,12 @@ describe('admin keys (TIP-1049)', () => {
2401
2446
  expect(restored.account).toBeUndefined()
2402
2447
  })
2403
2448
 
2404
- test('fromTuple: drops orphan account without isAdmin', () => {
2449
+ test('fromTuple: preserves a non-admin account binding', () => {
2405
2450
  const restored = KeyAuthorization.fromTuple([
2406
2451
  ['0x01', '0x', address, '0x', [], [], '0x', '0x', account],
2407
2452
  ])
2408
- expect(restored.isAdmin).toBeUndefined()
2409
- expect(restored.account).toBeUndefined()
2453
+ expect(restored.isAdmin).toBe(false)
2454
+ expect(restored.account).toBe(account)
2410
2455
  })
2411
2456
 
2412
2457
  test('fromTuple: extracts isAdmin + account together', () => {
@@ -2441,6 +2486,20 @@ describe('admin keys (TIP-1049)', () => {
2441
2486
  expect(restored.account).toBe(account)
2442
2487
  })
2443
2488
 
2489
+ test('serialize/deserialize: roundtrip with non-admin account binding', () => {
2490
+ const authorization = KeyAuthorization.from({
2491
+ address,
2492
+ account,
2493
+ chainId: 1n,
2494
+ isAdmin: false,
2495
+ type: 'secp256k1',
2496
+ })
2497
+ const serialized = KeyAuthorization.serialize(authorization)
2498
+ const restored = KeyAuthorization.deserialize(serialized)
2499
+ expect(restored.isAdmin).toBe(false)
2500
+ expect(restored.account).toBe(account)
2501
+ })
2502
+
2444
2503
  test('toRpc/fromRpc: roundtrip with isAdmin + account', () => {
2445
2504
  const authorization = KeyAuthorization.from(
2446
2505
  {
@@ -2471,15 +2530,15 @@ describe('admin keys (TIP-1049)', () => {
2471
2530
  expect(restored.account).toBeUndefined()
2472
2531
  })
2473
2532
 
2474
- test('fromRpc: drops orphan account without isAdmin', () => {
2533
+ test('fromRpc: preserves a non-admin account binding', () => {
2475
2534
  const authorization = KeyAuthorization.from(
2476
2535
  { address, chainId: 1n, type: 'secp256k1' },
2477
2536
  { signature: SignatureEnvelope.from(signature_secp256k1) },
2478
2537
  )
2479
2538
  const rpc = KeyAuthorization.toRpc(authorization)
2480
2539
  const restored = KeyAuthorization.fromRpc({ ...rpc, account })
2481
- expect(restored.isAdmin).toBeUndefined()
2482
- expect(restored.account).toBeUndefined()
2540
+ expect(restored.isAdmin).toBe(false)
2541
+ expect(restored.account).toBe(account)
2483
2542
  })
2484
2543
 
2485
2544
  test('hash: changes when admin pair is added', () => {