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
@@ -572,6 +572,172 @@ describe('toBytes', () => {
572
572
  })
573
573
  })
574
574
 
575
+ describe('toCompactBytes', () => {
576
+ test('default', () => {
577
+ const bytes = Signature.toCompactBytes({
578
+ r: '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf',
579
+ s: '0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
580
+ yParity: 1,
581
+ })
582
+ expect(bytes.length).toBe(64)
583
+ expect(Hex.fromBytes(bytes)).toBe(
584
+ '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
585
+ )
586
+ })
587
+
588
+ test('behavior: r/s shorter than 32 bytes are left-padded, not right-padded', () => {
589
+ const bytes = Signature.toCompactBytes({
590
+ r: '0x01',
591
+ s: '0x02',
592
+ yParity: 0,
593
+ })
594
+ // `r` occupies the first 32 bytes and must be the big-endian integer `1`
595
+ // (left-padded), not `1` shifted into the top byte (right-padded).
596
+ expect(Hex.fromBytes(bytes.subarray(0, 32))).toBe(
597
+ '0x0000000000000000000000000000000000000000000000000000000000000001',
598
+ )
599
+ expect(Hex.fromBytes(bytes.subarray(32, 64))).toBe(
600
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
601
+ )
602
+ })
603
+
604
+ test('behavior: round-trips through recoverPublicKey for a signature whose r has a leading zero byte', async () => {
605
+ const { Secp256k1 } = await import('ox')
606
+ const payload = `0x${'11'.repeat(32)}` as const
607
+ const privateKey = Hex.fromNumber(4n * 123456789013n + 7n, { size: 32 })
608
+ const signature = Secp256k1.sign({ payload, privateKey })
609
+ // sanity: this fixture's `r` genuinely has a leading zero byte
610
+ expect(signature.r.startsWith('0x00')).toBe(true)
611
+ const expectedPublicKey = Secp256k1.recoverPublicKey({ payload, signature })
612
+
613
+ // simulate an external source (RPC/subgraph/DB) that stores `r` in
614
+ // minimal (non-zero-padded) form, as the `Hex.Hex` type permits
615
+ const minimalR = `0x${signature.r.slice(4)}` as const
616
+ const recoveredBytes = Signature.toRecoveredBytes({
617
+ r: minimalR,
618
+ s: signature.s,
619
+ yParity: signature.yParity,
620
+ })
621
+ const roundTripped = Signature.fromRecoveredBytes(recoveredBytes)
622
+ const recoveredPublicKey = Secp256k1.recoverPublicKey({
623
+ payload,
624
+ signature: roundTripped,
625
+ })
626
+ expect(recoveredPublicKey.x).toBe(expectedPublicKey.x)
627
+ expect(recoveredPublicKey.y).toBe(expectedPublicKey.y)
628
+ })
629
+ })
630
+
631
+ describe('fromCompactBytes', () => {
632
+ test('default', () => {
633
+ const signature = Signature.fromCompactBytes(
634
+ Bytes.fromHex(
635
+ '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
636
+ ),
637
+ )
638
+ expect(signature).toStrictEqual({
639
+ r: '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf',
640
+ s: '0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
641
+ })
642
+ })
643
+
644
+ test('behavior: throws on a byte length other than 64', () => {
645
+ expect(() => Signature.fromCompactBytes(new Uint8Array(65)))
646
+ .toThrowErrorMatchingInlineSnapshot(`
647
+ [Signature.InvalidSerializedSizeError: Value \`0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\` is an invalid signature size.
648
+
649
+ Expected: 64 bytes.
650
+ Received 65 bytes.]
651
+ `)
652
+ })
653
+ })
654
+
655
+ describe('toRecoveredBytes', () => {
656
+ test('default', () => {
657
+ const bytes = Signature.toRecoveredBytes({
658
+ r: '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf',
659
+ s: '0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
660
+ yParity: 1,
661
+ })
662
+ expect(bytes.length).toBe(65)
663
+ expect(bytes[0]).toBe(1)
664
+ expect(Hex.fromBytes(bytes.subarray(1))).toBe(
665
+ '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
666
+ )
667
+ })
668
+
669
+ test('behavior: r/s shorter than 32 bytes are left-padded, not right-padded', () => {
670
+ const bytes = Signature.toRecoveredBytes({
671
+ r: '0x01',
672
+ s: '0x02',
673
+ yParity: 0,
674
+ })
675
+ expect(Hex.fromBytes(bytes.subarray(1, 33))).toBe(
676
+ '0x0000000000000000000000000000000000000000000000000000000000000001',
677
+ )
678
+ expect(Hex.fromBytes(bytes.subarray(33, 65))).toBe(
679
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
680
+ )
681
+ })
682
+
683
+ test('behavior: throws before coercing an out-of-range yParity', () => {
684
+ expect(() =>
685
+ Signature.toRecoveredBytes({
686
+ r: '0x01',
687
+ s: '0x02',
688
+ yParity: 256,
689
+ }),
690
+ ).toThrowErrorMatchingInlineSnapshot(
691
+ `[Signature.InvalidYParityError: Value \`256\` is an invalid y-parity value. Y-parity must be 0 or 1.]`,
692
+ )
693
+ })
694
+ })
695
+
696
+ describe('fromRecoveredBytes', () => {
697
+ test('default', () => {
698
+ const bytes = new Uint8Array(65)
699
+ bytes[0] = 1
700
+ bytes.set(
701
+ Bytes.fromHex(
702
+ '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf',
703
+ ),
704
+ 1,
705
+ )
706
+ bytes.set(
707
+ Bytes.fromHex(
708
+ '0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
709
+ ),
710
+ 33,
711
+ )
712
+ const signature = Signature.fromRecoveredBytes(bytes)
713
+ expect(signature).toStrictEqual({
714
+ r: '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf',
715
+ s: '0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8',
716
+ yParity: 1,
717
+ })
718
+ })
719
+
720
+ test('behavior: throws on a byte length other than 65', () => {
721
+ expect(() => Signature.fromRecoveredBytes(new Uint8Array(64)))
722
+ .toThrowErrorMatchingInlineSnapshot(`
723
+ [Signature.InvalidSerializedSizeError: Value \`0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0\` is an invalid signature size.
724
+
725
+ Expected: 65 bytes.
726
+ Received 64 bytes.]
727
+ `)
728
+ })
729
+
730
+ test('behavior: throws on an out-of-range yParity byte', () => {
731
+ const bytes = new Uint8Array(65)
732
+ bytes[0] = 7
733
+ expect(() =>
734
+ Signature.fromRecoveredBytes(bytes),
735
+ ).toThrowErrorMatchingInlineSnapshot(
736
+ `[Signature.InvalidYParityError: Value \`7\` is an invalid y-parity value. Y-parity must be 0 or 1.]`,
737
+ )
738
+ })
739
+ })
740
+
575
741
  describe('toDerHex', () => {
576
742
  test('default', () => {
577
743
  const signature = Signature.from({
@@ -324,6 +324,53 @@ describe('createMessage', () => {
324
324
  `)
325
325
  })
326
326
 
327
+ test('behavior: uri with no path (authority only)', () => {
328
+ vi.useFakeTimers()
329
+ vi.setSystemTime(new Date(Date.UTC(2023, 1, 1)))
330
+
331
+ expect(Siwe.createMessage({ ...message, uri: 'https://example.com' }))
332
+ .toMatchInlineSnapshot(`
333
+ "example.com wants you to sign in with your Ethereum account:
334
+ 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e
335
+
336
+
337
+ URI: https://example.com
338
+ Version: 1
339
+ Chain ID: 1
340
+ Nonce: foobarbaz
341
+ Issued At: 2023-02-01T00:00:00.000Z"
342
+ `)
343
+
344
+ vi.useRealTimers()
345
+ })
346
+
347
+ test('behavior: resources entry with no path (authority only)', () => {
348
+ vi.useFakeTimers()
349
+ vi.setSystemTime(new Date(Date.UTC(2023, 1, 1)))
350
+
351
+ expect(
352
+ Siwe.createMessage({
353
+ ...message,
354
+ resources: ['https://example.com', 'https://example.com/foo'],
355
+ }),
356
+ ).toMatchInlineSnapshot(`
357
+ "example.com wants you to sign in with your Ethereum account:
358
+ 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e
359
+
360
+
361
+ URI: https://example.com/path
362
+ Version: 1
363
+ Chain ID: 1
364
+ Nonce: foobarbaz
365
+ Issued At: 2023-02-01T00:00:00.000Z
366
+ Resources:
367
+ - https://example.com
368
+ - https://example.com/foo"
369
+ `)
370
+
371
+ vi.useRealTimers()
372
+ })
373
+
327
374
  test('behavior: invalid version', () => {
328
375
  expect(() =>
329
376
  // @ts-expect-error
@@ -373,7 +420,7 @@ describe('createMessage', () => {
373
420
  - Every resource must be a RFC 3986 URI.
374
421
  - See https://www.rfc-editor.org/rfc/rfc3986
375
422
 
376
- Provided value: https://example.com]
423
+ Provided value: foo]
377
424
  `)
378
425
  })
379
426
 
@@ -426,6 +473,58 @@ describe('isUri', () => {
426
473
  )
427
474
  })
428
475
 
476
+ test('behavior: authority with empty path (bare origin)', () => {
477
+ expect(Siwe.isUri('https://example.com')).toMatchInlineSnapshot(
478
+ `"https://example.com"`,
479
+ )
480
+ })
481
+
482
+ test('behavior: authority with empty path and port', () => {
483
+ expect(Siwe.isUri('https://example.com:8080')).toMatchInlineSnapshot(
484
+ `"https://example.com:8080"`,
485
+ )
486
+ })
487
+
488
+ test('behavior: authority with empty path and query', () => {
489
+ expect(Siwe.isUri('https://example.com?a=1')).toMatchInlineSnapshot(
490
+ `"https://example.com?a=1"`,
491
+ )
492
+ })
493
+
494
+ test('behavior: authority with empty path and fragment', () => {
495
+ expect(Siwe.isUri('https://example.com#top')).toMatchInlineSnapshot(
496
+ `"https://example.com#top"`,
497
+ )
498
+ })
499
+
500
+ test('behavior: authority with empty path (non-http scheme)', () => {
501
+ expect(Siwe.isUri('ipfs://bafybeiabc')).toMatchInlineSnapshot(
502
+ `"ipfs://bafybeiabc"`,
503
+ )
504
+ })
505
+
506
+ test('behavior: authority variants with empty path', () => {
507
+ expect([
508
+ Siwe.isUri('https://[2001:db8::1]'),
509
+ Siwe.isUri('https://user:pass@example.com:8080'),
510
+ Siwe.isUri('foo://[v1.fe]'),
511
+ ]).toMatchInlineSnapshot(`
512
+ [
513
+ "https://[2001:db8::1]",
514
+ "https://user:pass@example.com:8080",
515
+ "foo://[v1.fe]",
516
+ ]
517
+ `)
518
+ })
519
+
520
+ test.each([
521
+ 'https://example.com:bad',
522
+ 'https://[:::]',
523
+ 'https://user@@example.com',
524
+ ])('behavior: invalid authority with empty path `%s`', (uri) => {
525
+ expect(Siwe.isUri(uri)).toMatchInlineSnapshot(`false`)
526
+ })
527
+
429
528
  test('behavior: check for illegal characters', () => {
430
529
  expect(Siwe.isUri('^')).toBeFalsy()
431
530
  })
@@ -348,6 +348,8 @@ describe('fromRpc', () => {
348
348
  type: 'p256',
349
349
  },
350
350
  })
351
+ if (typeof result.signature === 'string')
352
+ throw new Error('Expected structured primitive signature')
351
353
  expect(result.signature.type).toBe('p256')
352
354
  expect(result.signature).toHaveProperty('prehash', true)
353
355
  expect(result.signature).toHaveProperty('publicKey')
@@ -370,6 +372,8 @@ describe('fromRpc', () => {
370
372
  '0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22222c226f726967696e223a2268747470733a2f2f6578616d706c652e636f6d222c2263726f73734f726967696e223a66616c73657d',
371
373
  },
372
374
  })
375
+ if (typeof result.signature === 'string')
376
+ throw new Error('Expected structured primitive signature')
373
377
  expect(result.signature.type).toBe('webAuthn')
374
378
  expect(result.signature).toHaveProperty('metadata')
375
379
  expect(result.signature).toHaveProperty('publicKey')
@@ -994,6 +998,8 @@ describe('toRpc', () => {
994
998
  type: 'p256',
995
999
  },
996
1000
  })
1001
+ if (typeof result.signature === 'string')
1002
+ throw new Error('Expected structured primitive signature')
997
1003
  expect(result.signature.type).toBe('p256')
998
1004
  })
999
1005
 
@@ -1019,6 +1025,8 @@ describe('toRpc', () => {
1019
1025
  type: 'webAuthn',
1020
1026
  },
1021
1027
  })
1028
+ if (typeof result.signature === 'string')
1029
+ throw new Error('Expected structured primitive signature')
1022
1030
  expect(result.signature.type).toBe('webAuthn')
1023
1031
  })
1024
1032
  })
@@ -1,7 +1,6 @@
1
1
  import { expectTypeOf, test } from 'vp/test'
2
2
  import * as KeyAuthorization from './KeyAuthorization.js'
3
- import * as MultisigConfig from './MultisigConfig.js'
4
- import type * as SignatureEnvelope from './SignatureEnvelope.js'
3
+ import * as SignatureEnvelope from './SignatureEnvelope.js'
5
4
 
6
5
  const authorization = {
7
6
  address: '0x1111111111111111111111111111111111111111',
@@ -20,26 +19,18 @@ const signature = {
20
19
 
21
20
  const multisig = {
22
21
  account: '0x2222222222222222222222222222222222222222',
23
- config: MultisigConfig.from({
22
+ config: {
23
+ salt: `0x${'00'.repeat(32)}`,
24
+ version: 0n,
25
+ threshold: 1,
24
26
  owners: [
25
- {
26
- owner: '0x1111111111111111111111111111111111111111',
27
- weight: 1,
28
- },
27
+ { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
29
28
  ],
30
- threshold: 1,
31
- version: 1n,
32
- }),
29
+ },
33
30
  signatures: [signature],
34
31
  type: 'multisig',
35
32
  } as const satisfies SignatureEnvelope.Multisig
36
33
 
37
- const keychain = {
38
- inner: signature,
39
- type: 'keychain',
40
- userAddress: authorization.address,
41
- } as const satisfies SignatureEnvelope.Keychain
42
-
43
34
  test('accepts primitive signatures', () => {
44
35
  const signed = KeyAuthorization.from(authorization, { signature })
45
36
 
@@ -48,62 +39,13 @@ test('accepts primitive signatures', () => {
48
39
  >()
49
40
  })
50
41
 
51
- test('accepts multisig signatures', () => {
52
- const signed = KeyAuthorization.from(authorization, { signature: multisig })
53
-
54
- expectTypeOf(signed.signature).toMatchTypeOf<
55
- KeyAuthorization.Signed['signature']
56
- >()
57
-
58
- const multisigRpc = {
59
- account: multisig.account,
60
- config: MultisigConfig.toRpc(multisig.config),
61
- signatures: [
62
- {
63
- r: '0x01',
64
- s: '0x02',
65
- type: 'secp256k1',
66
- yParity: '0x0',
67
- },
68
- ],
69
- } as const satisfies SignatureEnvelope.MultisigRpc
70
-
71
- const rpc: KeyAuthorization.Rpc = {
72
- chainId: '0x1',
73
- expiry: null,
74
- keyId: authorization.address,
75
- keyType: authorization.type,
76
- signature: multisigRpc,
77
- }
78
-
79
- expectTypeOf(rpc).toEqualTypeOf<KeyAuthorization.Rpc>()
80
- })
81
-
82
- test('rejects keychain signatures', () => {
83
- KeyAuthorization.from(authorization, {
84
- // @ts-expect-error Key authorizations do not accept keychain signatures.
85
- signature: keychain,
86
- })
87
-
88
- const keychainRpc = {
89
- signature: {
90
- r: '0x01',
91
- s: '0x02',
92
- type: 'secp256k1',
93
- yParity: '0x0',
94
- },
95
- type: 'keychain',
96
- userAddress: authorization.address,
97
- } as const satisfies SignatureEnvelope.KeychainRpc
98
-
99
- const rpc: KeyAuthorization.Rpc = {
100
- chainId: '0x1',
101
- expiry: null,
102
- keyId: authorization.address,
103
- keyType: authorization.type,
104
- // @ts-expect-error Key authorizations do not accept keychain RPC signatures.
105
- signature: keychainRpc,
106
- }
107
-
108
- expectTypeOf(rpc).toEqualTypeOf<KeyAuthorization.Rpc>()
42
+ test('accepts multisig signatures and account-bound grants', () => {
43
+ const signed = KeyAuthorization.from(
44
+ { ...authorization, account: multisig.account, type: 'multisig' },
45
+ { signature: multisig },
46
+ )
47
+ expectTypeOf(signed).toMatchTypeOf<KeyAuthorization.Signed>()
48
+ expectTypeOf(
49
+ KeyAuthorization.toRpc(signed),
50
+ ).toMatchTypeOf<KeyAuthorization.Rpc>()
109
51
  })
@@ -69,6 +69,8 @@ const signature_multisig = {
69
69
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
70
70
  type: 'multisig',
71
71
  } as const satisfies SignatureEnvelope.Multisig
72
+ const signature_multisig_rpc =
73
+ '0xf89794be95c3f554e9fc85ec51be69a3d807a0d55bcf2cf83ba000000000000000000000000000000000000000000000000000000000000000000101d7d694f39fd6e51aad88f6f4ce6ab8827279cfffb9226601f843b841fa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d01c' as const
72
74
 
73
75
  const signature_keychain = {
74
76
  inner: SignatureEnvelope.from(signature_secp256k1),
@@ -76,7 +78,66 @@ const signature_keychain = {
76
78
  userAddress: address,
77
79
  } as const satisfies SignatureEnvelope.Keychain
78
80
 
81
+ describe('multisig account binding', () => {
82
+ test('rejects unbound grants before signing or encoding', () => {
83
+ const authorization = { address, chainId: 1n, type: 'multisig' } as const
84
+ const signed = {
85
+ ...authorization,
86
+ signature: SignatureEnvelope.from(signature_secp256k1),
87
+ }
88
+ for (const encode of [
89
+ () => KeyAuthorization.from(authorization),
90
+ () => KeyAuthorization.toTuple(authorization),
91
+ () => KeyAuthorization.getSignPayload(authorization),
92
+ () => KeyAuthorization.serialize(authorization),
93
+ () => KeyAuthorization.toRpc(signed),
94
+ ])
95
+ expect(encode).toThrowErrorMatchingInlineSnapshot(
96
+ `[KeyAuthorization.MissingAccountError: Multisig key grants require a parent account binding.]`,
97
+ )
98
+ })
99
+
100
+ test('rejects unbound RPC and RLP grants', () => {
101
+ const signed = KeyAuthorization.from(
102
+ { account: address, address, chainId: 1n, type: 'multisig' },
103
+ { signature: signature_secp256k1 },
104
+ )
105
+ const rpc = KeyAuthorization.toRpc(signed)
106
+ for (const account of [undefined, null])
107
+ expect(() => KeyAuthorization.fromRpc({ ...rpc, account })).toThrowError(
108
+ KeyAuthorization.MissingAccountError,
109
+ )
110
+ expect(() =>
111
+ KeyAuthorization.fromTuple([['0x01', '0x03', address]]),
112
+ ).toThrowError(KeyAuthorization.MissingAccountError)
113
+ expect(KeyAuthorization.fromRpc(rpc).account).toBe(address)
114
+ expect(
115
+ KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)).account,
116
+ ).toBe(address)
117
+ })
118
+ })
119
+
79
120
  describe('from', () => {
121
+ test('accepts multisig grants and delegates', () => {
122
+ const signed = KeyAuthorization.from(
123
+ {
124
+ address,
125
+ chainId: 1n,
126
+ type: 'multisig',
127
+ account: signature_multisig.account,
128
+ },
129
+ { signature: signature_multisig },
130
+ )
131
+ expect(
132
+ KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)),
133
+ ).toEqual(signed)
134
+ expect(KeyAuthorization.fromRpc(KeyAuthorization.toRpc(signed))).toEqual(
135
+ signed,
136
+ )
137
+ })
138
+ })
139
+
140
+ describe('fromRpc', () => {
80
141
  test('default', () => {
81
142
  const authorization = KeyAuthorization.from({
82
143
  address,
@@ -502,16 +563,42 @@ describe('from', () => {
502
563
  })
503
564
 
504
565
  describe('fromRpc', () => {
505
- test('multisig', () => {
566
+ test('behavior: multisig', () => {
506
567
  const authorization = KeyAuthorization.fromRpc({
507
568
  chainId: '0x1',
508
569
  expiry: null,
509
570
  keyId: address,
510
571
  keyType: 'secp256k1',
511
- signature: SignatureEnvelope.toRpc(signature_multisig),
572
+ signature: signature_multisig_rpc,
512
573
  })
513
574
 
514
- expect(authorization.signature).toEqual(signature_multisig)
575
+ expect(authorization.signature).toMatchInlineSnapshot(`
576
+ {
577
+ "account": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
578
+ "config": {
579
+ "owners": [
580
+ {
581
+ "owner": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
582
+ "weight": 1,
583
+ },
584
+ ],
585
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
586
+ "threshold": 1,
587
+ "version": 1n,
588
+ },
589
+ "signatures": [
590
+ {
591
+ "signature": {
592
+ "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7",
593
+ "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0",
594
+ "yParity": 1,
595
+ },
596
+ "type": "secp256k1",
597
+ },
598
+ ],
599
+ "type": "multisig",
600
+ }
601
+ `)
515
602
  })
516
603
 
517
604
  test('rejects a keychain signature', () => {
@@ -1084,17 +1171,16 @@ describe('getSignPayload', () => {
1084
1171
  describe('deserialize', () => {
1085
1172
  test('example: matches the frozen multisig witness vector', () => {
1086
1173
  const serialized =
1087
- '0xf8f1f85382107980941eff47bc3a10a45d4b230b5d10e37751fe6aa718808080a0535353535353535353535353535353535353535353535353535353535353535380949dba7f426b711d4893c11611eacf7cc334e7146bb89a05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b8412cfed9350faf80e4115e3c4c967356b80a8e6ee42e4a49ab953c8c8a3537d8d8359bf035a9bea18f32b0da7129cdce3ce701abc09ad8919b93da21a0d57e21451b' as const
1174
+ '0xf8f1f85382107980941eff47bc3a10a45d4b230b5d10e37751fe6aa718808080a053535353535353535353535353535353535353535353535353535353535353538094c4a590afa7337e5cd5eb3aa60cacf91c5400044bb89a05f89794c4a590afa7337e5cd5eb3aa60cacf91c5400044bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b8412cfed9350faf80e4115e3c4c967356b80a8e6ee42e4a49ab953c8c8a3537d8d8359bf035a9bea18f32b0da7129cdce3ce701abc09ad8919b93da21a0d57e21451b' as const
1088
1175
  const authorization = KeyAuthorization.deserialize(serialized)
1089
1176
 
1090
1177
  expect(authorization).toMatchInlineSnapshot(`
1091
1178
  {
1092
- "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
1179
+ "account": "0xc4a590afa7337e5cd5eb3aa60cacf91c5400044b",
1093
1180
  "address": "0x1eff47bc3a10a45d4b230b5d10e37751fe6aa718",
1094
1181
  "chainId": 4217n,
1095
- "isAdmin": false,
1096
1182
  "signature": {
1097
- "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
1183
+ "account": "0xc4a590afa7337e5cd5eb3aa60cacf91c5400044b",
1098
1184
  "config": {
1099
1185
  "owners": [
1100
1186
  {
@@ -1306,6 +1392,19 @@ describe('serialize', () => {
1306
1392
  })
1307
1393
 
1308
1394
  describe('toRpc', () => {
1395
+ test('behavior: multisig', () => {
1396
+ const authorization = KeyAuthorization.toRpc({
1397
+ address,
1398
+ chainId: 1n,
1399
+ signature: signature_multisig,
1400
+ type: 'secp256k1',
1401
+ })
1402
+
1403
+ expect(authorization.signature).toMatchInlineSnapshot(
1404
+ `"${signature_multisig_rpc}"`,
1405
+ )
1406
+ })
1407
+
1309
1408
  test('secp256k1', () => {
1310
1409
  const authorization = KeyAuthorization.from({
1311
1410
  address,
@@ -2503,11 +2602,11 @@ describe('admin keys (TIP-1049)', () => {
2503
2602
  expect((authTuple as unknown as unknown[]).length).toBe(3)
2504
2603
  })
2505
2604
 
2506
- test('fromTuple: drops orphan isAdmin without account', () => {
2605
+ test('fromTuple: preserves isAdmin without account', () => {
2507
2606
  const restored = KeyAuthorization.fromTuple([
2508
2607
  ['0x01', '0x', address, '0x', [], [], '0x', '0x01'],
2509
2608
  ])
2510
- expect(restored.isAdmin).toBeUndefined()
2609
+ expect(restored.isAdmin).toBe(true)
2511
2610
  expect(restored.account).toBeUndefined()
2512
2611
  })
2513
2612
 
@@ -2515,7 +2614,7 @@ describe('admin keys (TIP-1049)', () => {
2515
2614
  const restored = KeyAuthorization.fromTuple([
2516
2615
  ['0x01', '0x', address, '0x', [], [], '0x', '0x', account],
2517
2616
  ])
2518
- expect(restored.isAdmin).toBe(false)
2617
+ expect(restored.isAdmin).toBeUndefined()
2519
2618
  expect(restored.account).toBe(account)
2520
2619
  })
2521
2620
 
@@ -2561,7 +2660,7 @@ describe('admin keys (TIP-1049)', () => {
2561
2660
  })
2562
2661
  const serialized = KeyAuthorization.serialize(authorization)
2563
2662
  const restored = KeyAuthorization.deserialize(serialized)
2564
- expect(restored.isAdmin).toBe(false)
2663
+ expect(restored.isAdmin).toBeUndefined()
2565
2664
  expect(restored.account).toBe(account)
2566
2665
  })
2567
2666
 
@@ -2584,14 +2683,14 @@ describe('admin keys (TIP-1049)', () => {
2584
2683
  expect(restored.account).toBe(account)
2585
2684
  })
2586
2685
 
2587
- test('fromRpc: drops orphan isAdmin without account', () => {
2686
+ test('fromRpc: preserves isAdmin without account', () => {
2588
2687
  const authorization = KeyAuthorization.from(
2589
2688
  { address, chainId: 1n, type: 'secp256k1' },
2590
2689
  { signature: SignatureEnvelope.from(signature_secp256k1) },
2591
2690
  )
2592
2691
  const rpc = KeyAuthorization.toRpc(authorization)
2593
2692
  const restored = KeyAuthorization.fromRpc({ ...rpc, isAdmin: true })
2594
- expect(restored.isAdmin).toBeUndefined()
2693
+ expect(restored.isAdmin).toBe(true)
2595
2694
  expect(restored.account).toBeUndefined()
2596
2695
  })
2597
2696
 
@@ -2602,7 +2701,7 @@ describe('admin keys (TIP-1049)', () => {
2602
2701
  )
2603
2702
  const rpc = KeyAuthorization.toRpc(authorization)
2604
2703
  const restored = KeyAuthorization.fromRpc({ ...rpc, account })
2605
- expect(restored.isAdmin).toBe(false)
2704
+ expect(restored.isAdmin).toBeUndefined()
2606
2705
  expect(restored.account).toBe(account)
2607
2706
  })
2608
2707