ox 0.13.2 → 0.14.1

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 (85) hide show
  1. package/Bech32m/package.json +6 -0
  2. package/CHANGELOG.md +25 -0
  3. package/_cjs/core/Bech32m.js +205 -0
  4. package/_cjs/core/Bech32m.js.map +1 -0
  5. package/_cjs/index.js +3 -2
  6. package/_cjs/index.js.map +1 -1
  7. package/_cjs/tempo/AuthorizationTempo.js +7 -2
  8. package/_cjs/tempo/AuthorizationTempo.js.map +1 -1
  9. package/_cjs/tempo/KeyAuthorization.js +22 -8
  10. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  11. package/_cjs/tempo/SignatureEnvelope.js +39 -4
  12. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  13. package/_cjs/tempo/TempoAddress.js +49 -41
  14. package/_cjs/tempo/TempoAddress.js.map +1 -1
  15. package/_cjs/tempo/TokenId.js +8 -5
  16. package/_cjs/tempo/TokenId.js.map +1 -1
  17. package/_cjs/tempo/TransactionRequest.js +2 -1
  18. package/_cjs/tempo/TransactionRequest.js.map +1 -1
  19. package/_cjs/tempo/TxEnvelopeTempo.js +17 -4
  20. package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
  21. package/_cjs/tempo/index.js.map +1 -1
  22. package/_cjs/version.js +1 -1
  23. package/_esm/core/Bech32m.js +238 -0
  24. package/_esm/core/Bech32m.js.map +1 -0
  25. package/_esm/index.js +24 -0
  26. package/_esm/index.js.map +1 -1
  27. package/_esm/tempo/AuthorizationTempo.js +24 -19
  28. package/_esm/tempo/AuthorizationTempo.js.map +1 -1
  29. package/_esm/tempo/KeyAuthorization.js +38 -13
  30. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  31. package/_esm/tempo/SignatureEnvelope.js +43 -6
  32. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  33. package/_esm/tempo/TempoAddress.js +79 -48
  34. package/_esm/tempo/TempoAddress.js.map +1 -1
  35. package/_esm/tempo/TokenId.js +9 -6
  36. package/_esm/tempo/TokenId.js.map +1 -1
  37. package/_esm/tempo/TransactionRequest.js +2 -1
  38. package/_esm/tempo/TransactionRequest.js.map +1 -1
  39. package/_esm/tempo/TxEnvelopeTempo.js +66 -15
  40. package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
  41. package/_esm/tempo/index.js +11 -10
  42. package/_esm/tempo/index.js.map +1 -1
  43. package/_esm/version.js +1 -1
  44. package/_types/core/Bech32m.d.ts +93 -0
  45. package/_types/core/Bech32m.d.ts.map +1 -0
  46. package/_types/index.d.ts +24 -0
  47. package/_types/index.d.ts.map +1 -1
  48. package/_types/tempo/AuthorizationTempo.d.ts +19 -19
  49. package/_types/tempo/AuthorizationTempo.d.ts.map +1 -1
  50. package/_types/tempo/KeyAuthorization.d.ts +21 -9
  51. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  52. package/_types/tempo/SignatureEnvelope.d.ts +30 -7
  53. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  54. package/_types/tempo/TempoAddress.d.ts +51 -15
  55. package/_types/tempo/TempoAddress.d.ts.map +1 -1
  56. package/_types/tempo/TokenId.d.ts +6 -5
  57. package/_types/tempo/TokenId.d.ts.map +1 -1
  58. package/_types/tempo/TransactionRequest.d.ts +2 -1
  59. package/_types/tempo/TransactionRequest.d.ts.map +1 -1
  60. package/_types/tempo/TxEnvelopeTempo.d.ts +64 -17
  61. package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  62. package/_types/tempo/index.d.ts +11 -10
  63. package/_types/tempo/index.d.ts.map +1 -1
  64. package/_types/version.d.ts +1 -1
  65. package/core/Bech32m.ts +263 -0
  66. package/index.ts +24 -2
  67. package/package.json +6 -1
  68. package/tempo/AuthorizationTempo.test.ts +13 -0
  69. package/tempo/AuthorizationTempo.ts +27 -21
  70. package/tempo/KeyAuthorization.test.ts +95 -23
  71. package/tempo/KeyAuthorization.ts +44 -25
  72. package/tempo/PoolId.test.ts +9 -0
  73. package/tempo/SignatureEnvelope.test.ts +123 -8
  74. package/tempo/SignatureEnvelope.ts +82 -9
  75. package/tempo/TempoAddress.test.ts +70 -14
  76. package/tempo/TempoAddress.ts +95 -67
  77. package/tempo/TokenId.test.ts +14 -0
  78. package/tempo/TokenId.ts +13 -10
  79. package/tempo/Transaction.test.ts +4 -2
  80. package/tempo/TransactionRequest.ts +3 -2
  81. package/tempo/TxEnvelopeTempo.test.ts +79 -3
  82. package/tempo/TxEnvelopeTempo.ts +86 -19
  83. package/tempo/e2e.test.ts +33 -9
  84. package/tempo/index.ts +15 -10
  85. package/version.ts +1 -1
@@ -5,6 +5,7 @@ import * as Hex from '../core/Hex.js'
5
5
  import type { Compute } from '../core/internal/types.js'
6
6
  import * as Rlp from '../core/Rlp.js'
7
7
  import * as SignatureEnvelope from './SignatureEnvelope.js'
8
+ import * as TempoAddress from './TempoAddress.js'
8
9
 
9
10
  /**
10
11
  * Key authorization for provisioning access keys.
@@ -33,9 +34,9 @@ export type KeyAuthorization<
33
34
  numberType = number,
34
35
  > = {
35
36
  /** Address derived from the public key of the key type. */
36
- address: Address.Address
37
- /** Chain ID for replay protection (0 = valid on any chain). */
38
- chainId?: bigintType | undefined
37
+ address: TempoAddress.Address
38
+ /** Chain ID for replay protection. */
39
+ chainId: bigintType
39
40
  /** Unix timestamp when key expires (0 = never expires). */
40
41
  expiry?: numberType | null | undefined
41
42
  /** TIP20 spending limits for this key. */
@@ -107,7 +108,7 @@ export type Tuple<signed extends boolean = boolean> = signed extends true
107
108
  */
108
109
  export type TokenLimit<bigintType = bigint> = {
109
110
  /** Address of the TIP-20 token. */
110
- token: Address.Address
111
+ token: TempoAddress.Address
111
112
  /** Maximum spending amount for this token (enforced over the key's lifetime). */
112
113
  limit: bigintType
113
114
  }
@@ -136,6 +137,7 @@ export type TokenLimit<bigintType = bigint> = {
136
137
  *
137
138
  * const authorization = KeyAuthorization.from({
138
139
  * address,
140
+ * chainId: 4217n,
139
141
  * expiry: 1234567890,
140
142
  * type: 'secp256k1',
141
143
  * limits: [{
@@ -157,6 +159,7 @@ export type TokenLimit<bigintType = bigint> = {
157
159
  *
158
160
  * const authorization = KeyAuthorization.from({
159
161
  * address,
162
+ * chainId: 4217n,
160
163
  * expiry: 1234567890,
161
164
  * type: 'p256',
162
165
  * limits: [{
@@ -181,6 +184,7 @@ export type TokenLimit<bigintType = bigint> = {
181
184
  *
182
185
  * const authorization = KeyAuthorization.from({
183
186
  * address,
187
+ * chainId: 4217n,
184
188
  * expiry: 1234567890,
185
189
  * type: 'secp256k1',
186
190
  * limits: [{
@@ -214,6 +218,7 @@ export type TokenLimit<bigintType = bigint> = {
214
218
  *
215
219
  * const authorization = KeyAuthorization.from({
216
220
  * address,
221
+ * chainId: 4217n,
217
222
  * expiry: 1234567890,
218
223
  * type: 'p256',
219
224
  * limits: [{
@@ -251,14 +256,28 @@ export function from<
251
256
  authorization: authorization | KeyAuthorization,
252
257
  options: from.Options<signature> = {},
253
258
  ): from.ReturnType<authorization, signature> {
254
- if (typeof authorization.expiry === 'string')
255
- return fromRpc(authorization as Rpc) as never
259
+ if ('keyId' in authorization) return fromRpc(authorization as Rpc) as never
260
+ const auth = authorization as KeyAuthorization & {
261
+ limits?: readonly { token: TempoAddress.Address; limit: bigint }[]
262
+ }
263
+ const resolved = {
264
+ ...auth,
265
+ address: TempoAddress.resolve(auth.address as TempoAddress.Address),
266
+ ...(auth.limits
267
+ ? {
268
+ limits: auth.limits.map((l) => ({
269
+ ...l,
270
+ token: TempoAddress.resolve(l.token as TempoAddress.Address),
271
+ })),
272
+ }
273
+ : {}),
274
+ }
256
275
  if (options.signature)
257
276
  return {
258
- ...authorization,
277
+ ...resolved,
259
278
  signature: SignatureEnvelope.from(options.signature),
260
279
  } as never
261
- return authorization as never
280
+ return resolved as never
262
281
  }
263
282
 
264
283
  export declare namespace from {
@@ -296,6 +315,7 @@ export declare namespace from {
296
315
  * import { KeyAuthorization } from 'ox/tempo'
297
316
  *
298
317
  * const keyAuthorization = KeyAuthorization.fromRpc({
318
+ * chainId: '0x1079',
299
319
  * expiry: '0x174876e800',
300
320
  * keyId: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
301
321
  * keyType: 'secp256k1',
@@ -313,7 +333,7 @@ export declare namespace from {
313
333
  * @returns A signed {@link ox#AuthorizationTempo.AuthorizationTempo}.
314
334
  */
315
335
  export function fromRpc(authorization: Rpc): Signed {
316
- const { chainId = '0x0', keyId, expiry = 0, limits, keyType } = authorization
336
+ const { chainId, keyId, expiry = 0, limits, keyType } = authorization
317
337
  const signature = SignatureEnvelope.fromRpc(authorization.signature)
318
338
  return {
319
339
  address: keyId,
@@ -393,7 +413,7 @@ export function fromTuple<const tuple extends Tuple>(
393
413
  address: keyId,
394
414
  expiry: typeof expiry !== 'undefined' ? hexToNumber(expiry) : undefined,
395
415
  type: keyType,
396
- ...(chainId !== '0x' ? { chainId: Hex.toBigInt(chainId) } : {}),
416
+ chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
397
417
  ...(typeof expiry !== 'undefined' ? { expiry: hexToNumber(expiry) } : {}),
398
418
  ...(typeof limits !== 'undefined'
399
419
  ? {
@@ -436,6 +456,7 @@ export declare namespace fromTuple {
436
456
  *
437
457
  * const authorization = KeyAuthorization.from({
438
458
  * address,
459
+ * chainId: 4217n,
439
460
  * expiry: 1234567890,
440
461
  * type: 'secp256k1',
441
462
  * limits: [{
@@ -467,8 +488,9 @@ export declare namespace getSignPayload {
467
488
  * import { Value } from 'ox'
468
489
  *
469
490
  * const authorization = KeyAuthorization.from({
470
- * expiry: 1234567890,
471
491
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
492
+ * chainId: 4217n,
493
+ * expiry: 1234567890,
472
494
  * type: 'secp256k1',
473
495
  * limits: [{
474
496
  * token: '0x20c0000000000000000000000000000000000001',
@@ -504,8 +526,9 @@ export declare namespace deserialize {
504
526
  * import { Value } from 'ox'
505
527
  *
506
528
  * const authorization = KeyAuthorization.from({
507
- * expiry: 1234567890,
508
529
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
530
+ * chainId: 4217n,
531
+ * expiry: 1234567890,
509
532
  * type: 'secp256k1',
510
533
  * limits: [{
511
534
  * token: '0x20c0000000000000000000000000000000000001',
@@ -543,8 +566,9 @@ export declare namespace hash {
543
566
  * import { Value } from 'ox'
544
567
  *
545
568
  * const authorization = KeyAuthorization.from({
546
- * expiry: 1234567890,
547
569
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
570
+ * chainId: 4217n,
571
+ * expiry: 1234567890,
548
572
  * type: 'secp256k1',
549
573
  * limits: [{
550
574
  * token: '0x20c0000000000000000000000000000000000001',
@@ -579,8 +603,9 @@ export declare namespace serialize {
579
603
  * import { Value } from 'ox'
580
604
  *
581
605
  * const authorization = KeyAuthorization.toRpc({
582
- * expiry: 1234567890,
583
606
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
607
+ * chainId: 4217n,
608
+ * expiry: 1234567890,
584
609
  * type: 'secp256k1',
585
610
  * limits: [{
586
611
  * token: '0x20c0000000000000000000000000000000000001',
@@ -601,14 +626,7 @@ export declare namespace serialize {
601
626
  * @returns An RPC-formatted Key Authorization.
602
627
  */
603
628
  export function toRpc(authorization: Signed): Rpc {
604
- const {
605
- address,
606
- chainId = 0n,
607
- expiry,
608
- limits,
609
- type,
610
- signature,
611
- } = authorization
629
+ const { address, chainId, expiry, limits, type, signature } = authorization
612
630
 
613
631
  return {
614
632
  chainId: chainId === 0n ? '0x' : Hex.fromNumber(chainId),
@@ -617,7 +635,7 @@ export function toRpc(authorization: Signed): Rpc {
617
635
  token,
618
636
  limit: Hex.fromNumber(limit),
619
637
  })),
620
- keyId: address,
638
+ keyId: TempoAddress.resolve(address),
621
639
  signature: SignatureEnvelope.toRpc(signature),
622
640
  keyType: type,
623
641
  }
@@ -636,8 +654,9 @@ export declare namespace toRpc {
636
654
  * import { Value } from 'ox'
637
655
  *
638
656
  * const authorization = KeyAuthorization.from({
639
- * expiry: 1234567890,
640
657
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
658
+ * chainId: 4217n,
659
+ * expiry: 1234567890,
641
660
  * type: 'secp256k1',
642
661
  * limits: [{
643
662
  * token: '0x20c0000000000000000000000000000000000001',
@@ -660,7 +679,7 @@ export declare namespace toRpc {
660
679
  export function toTuple<const authorization extends KeyAuthorization>(
661
680
  authorization: authorization,
662
681
  ): toTuple.ReturnType<authorization> {
663
- const { address, chainId = 0n, expiry, limits } = authorization
682
+ const { address, chainId, expiry, limits } = authorization
664
683
  const signature = authorization.signature
665
684
  ? SignatureEnvelope.serialize(authorization.signature)
666
685
  : undefined
@@ -30,4 +30,13 @@ test('from', () => {
30
30
  validatorToken: 1n,
31
31
  })
32
32
  expect(poolId4).toBe(poolId1)
33
+
34
+ // Test with tempo address inputs
35
+ const tempoAddr0 = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh'
36
+ const tempoAddr1 = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
37
+ const poolId5 = PoolId.from({
38
+ userToken: tempoAddr0,
39
+ validatorToken: tempoAddr1,
40
+ })
41
+ expect(poolId5).toBe(poolId1)
33
42
  })
@@ -50,16 +50,19 @@ const signature_webauthn = SignatureEnvelope.from({
50
50
  const signature_keychain_secp256k1 = SignatureEnvelope.from({
51
51
  userAddress: '0x1234567890123456789012345678901234567890',
52
52
  inner: SignatureEnvelope.from(signature_secp256k1),
53
+ version: 'v2',
53
54
  })
54
55
 
55
56
  const signature_keychain_p256 = SignatureEnvelope.from({
56
57
  userAddress: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
57
58
  inner: signature_p256,
59
+ version: 'v2',
58
60
  })
59
61
 
60
62
  const signature_keychain_webauthn = SignatureEnvelope.from({
61
63
  userAddress: '0xfedcbafedcbafedcbafedcbafedcbafedcbafedc',
62
64
  inner: signature_webauthn,
65
+ version: 'v2',
63
66
  })
64
67
 
65
68
  describe('assert', () => {
@@ -509,7 +512,7 @@ describe('deserialize', () => {
509
512
  SignatureEnvelope.deserialize('0xdeadbeef'),
510
513
  ).toThrowErrorMatchingInlineSnapshot(
511
514
  `
512
- [SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xde. Expected 0x01 (P256) or 0x02 (WebAuthn)
515
+ [SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xde. Expected 0x01 (P256), 0x02 (WebAuthn), 0x03 (Keychain V1), or 0x04 (Keychain V2)
513
516
 
514
517
  Serialized: 0xdeadbeef]
515
518
  `,
@@ -669,7 +672,7 @@ describe('deserialize', () => {
669
672
  SignatureEnvelope.deserialize(unknownType),
670
673
  ).toThrowErrorMatchingInlineSnapshot(
671
674
  `
672
- [SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xff. Expected 0x01 (P256) or 0x02 (WebAuthn)
675
+ [SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xff. Expected 0x01 (P256), 0x02 (WebAuthn), 0x03 (Keychain V1), or 0x04 (Keychain V2)
673
676
 
674
677
  Serialized: 0xff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000]
675
678
  `,
@@ -712,6 +715,7 @@ describe('deserialize', () => {
712
715
  },
713
716
  "type": "keychain",
714
717
  "userAddress": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
718
+ "version": "v2",
715
719
  }
716
720
  `)
717
721
  })
@@ -742,6 +746,7 @@ describe('deserialize', () => {
742
746
  },
743
747
  "type": "keychain",
744
748
  "userAddress": "0xfedcbafedcbafedcbafedcbafedcbafedcbafedc",
749
+ "version": "v2",
745
750
  }
746
751
  `)
747
752
  })
@@ -1035,6 +1040,61 @@ describe('from', () => {
1035
1040
 
1036
1041
  expect(envelope.type).toBe('keychain')
1037
1042
  })
1043
+
1044
+ test('behavior: computes keyId from p256 inner publicKey', () => {
1045
+ const envelope = SignatureEnvelope.from({
1046
+ userAddress: '0x1234567890123456789012345678901234567890',
1047
+ inner: signature_p256,
1048
+ })
1049
+
1050
+ expect(envelope.keyId).toBe(Address.fromPublicKey(publicKey))
1051
+ })
1052
+
1053
+ test('behavior: computes keyId from webAuthn inner publicKey', () => {
1054
+ const envelope = SignatureEnvelope.from({
1055
+ userAddress: '0x1234567890123456789012345678901234567890',
1056
+ inner: signature_webauthn,
1057
+ })
1058
+
1059
+ expect(envelope.keyId).toBe(Address.fromPublicKey(publicKey))
1060
+ })
1061
+
1062
+ test('behavior: computes keyId from secp256k1 inner with payload', () => {
1063
+ const privateKey = Secp256k1.randomPrivateKey()
1064
+ const accessKeyPublicKey = Secp256k1.getPublicKey({ privateKey })
1065
+ const payload = '0xdeadbeef'
1066
+ const sig = Secp256k1.sign({ payload, privateKey })
1067
+
1068
+ const envelope = SignatureEnvelope.from(
1069
+ {
1070
+ userAddress: '0x1234567890123456789012345678901234567890',
1071
+ inner: SignatureEnvelope.from(sig),
1072
+ },
1073
+ { payload },
1074
+ )
1075
+
1076
+ expect(envelope.keyId).toBe(Address.fromPublicKey(accessKeyPublicKey))
1077
+ })
1078
+
1079
+ test('behavior: does not compute keyId for secp256k1 without payload', () => {
1080
+ const envelope = SignatureEnvelope.from({
1081
+ userAddress: '0x1234567890123456789012345678901234567890',
1082
+ inner: SignatureEnvelope.from(signature_secp256k1),
1083
+ })
1084
+
1085
+ expect(envelope.keyId).toBeUndefined()
1086
+ })
1087
+
1088
+ test('behavior: preserves explicit keyId', () => {
1089
+ const explicitKeyId = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
1090
+ const envelope = SignatureEnvelope.from({
1091
+ userAddress: '0x1234567890123456789012345678901234567890',
1092
+ inner: signature_p256,
1093
+ keyId: explicitKeyId,
1094
+ })
1095
+
1096
+ expect(envelope.keyId).toBe(explicitKeyId)
1097
+ })
1038
1098
  })
1039
1099
  })
1040
1100
 
@@ -1250,8 +1310,8 @@ describe('serialize', () => {
1250
1310
  // Should be: 1 (type) + 20 (address) + 65 (secp256k1 signature)
1251
1311
  expect(Hex.size(serialized)).toBe(1 + 20 + 65)
1252
1312
 
1253
- // First byte should be Keychain type identifier (0x03)
1254
- expect(Hex.slice(serialized, 0, 1)).toBe('0x03')
1313
+ // First byte should be Keychain V2 type identifier (0x04)
1314
+ expect(Hex.slice(serialized, 0, 1)).toBe('0x04')
1255
1315
 
1256
1316
  // Next 20 bytes should be the user address (without '0x')
1257
1317
  expect(Hex.slice(serialized, 1, 21)).toBe(
@@ -1265,8 +1325,8 @@ describe('serialize', () => {
1265
1325
  // Should be: 1 (type) + 20 (address) + 130 (p256 signature with type)
1266
1326
  expect(Hex.size(serialized)).toBe(1 + 20 + 130)
1267
1327
 
1268
- // First byte should be Keychain type identifier (0x03)
1269
- expect(Hex.slice(serialized, 0, 1)).toBe('0x03')
1328
+ // First byte should be Keychain V2 type identifier (0x04)
1329
+ expect(Hex.slice(serialized, 0, 1)).toBe('0x04')
1270
1330
 
1271
1331
  // Next 20 bytes should be the user address (without '0x')
1272
1332
  expect(Hex.slice(serialized, 1, 21)).toBe(
@@ -1284,8 +1344,8 @@ describe('serialize', () => {
1284
1344
  signature_keychain_webauthn,
1285
1345
  )
1286
1346
 
1287
- // First byte should be Keychain type identifier (0x03)
1288
- expect(Hex.slice(serialized, 0, 1)).toBe('0x03')
1347
+ // First byte should be Keychain V2 type identifier (0x04)
1348
+ expect(Hex.slice(serialized, 0, 1)).toBe('0x04')
1289
1349
 
1290
1350
  // Should contain the user address
1291
1351
  expect(Hex.slice(serialized, 1, 21)).toBe(
@@ -1519,6 +1579,7 @@ describe('serialize', () => {
1519
1579
  },
1520
1580
  "type": "keychain",
1521
1581
  "userAddress": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
1582
+ "version": "v2",
1522
1583
  }
1523
1584
  `)
1524
1585
  })
@@ -1549,6 +1610,7 @@ describe('serialize', () => {
1549
1610
  },
1550
1611
  "type": "keychain",
1551
1612
  "userAddress": "0xfedcbafedcbafedcbafedcbafedcbafedcbafedc",
1613
+ "version": "v2",
1552
1614
  }
1553
1615
  `)
1554
1616
  })
@@ -2228,6 +2290,24 @@ describe('fromRpc', () => {
2228
2290
  },
2229
2291
  })
2230
2292
  })
2293
+
2294
+ test('behavior: preserves keyId from RPC', () => {
2295
+ const rpc: SignatureEnvelope.KeychainRpc = {
2296
+ type: 'keychain',
2297
+ userAddress: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
2298
+ keyId: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
2299
+ signature: {
2300
+ type: 'secp256k1',
2301
+ r: '0xa2bb71146c20ce932456c043ebb2973ed205e07cd32c35a60bdefca1285fd132',
2302
+ s: '0x7cba10692bccdbfba9a215418443c2903dbee6fe5cb55c91172e47efc607840e',
2303
+ yParity: '0x1',
2304
+ },
2305
+ }
2306
+
2307
+ const envelope = SignatureEnvelope.fromRpc(rpc)
2308
+
2309
+ expect(envelope.keyId).toBe('0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c')
2310
+ })
2231
2311
  })
2232
2312
  })
2233
2313
 
@@ -2345,6 +2425,41 @@ describe('toRpc', () => {
2345
2425
  expect(typeof rpc.signature.pubKeyX).toBe('string')
2346
2426
  expect(typeof rpc.signature.webauthnData).toBe('string')
2347
2427
  })
2428
+
2429
+ test('behavior: preserves keyId in toRpc', () => {
2430
+ const envelope: SignatureEnvelope.Keychain = {
2431
+ type: 'keychain',
2432
+ userAddress: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
2433
+ keyId: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
2434
+ inner: {
2435
+ signature: signature_secp256k1,
2436
+ type: 'secp256k1',
2437
+ },
2438
+ }
2439
+
2440
+ const rpc = SignatureEnvelope.toRpc(
2441
+ envelope,
2442
+ ) as SignatureEnvelope.KeychainRpc
2443
+
2444
+ expect(rpc.keyId).toBe('0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c')
2445
+ })
2446
+
2447
+ test('behavior: omits keyId in toRpc when not set', () => {
2448
+ const envelope: SignatureEnvelope.Keychain = {
2449
+ type: 'keychain',
2450
+ userAddress: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
2451
+ inner: {
2452
+ signature: signature_secp256k1,
2453
+ type: 'secp256k1',
2454
+ },
2455
+ }
2456
+
2457
+ const rpc = SignatureEnvelope.toRpc(
2458
+ envelope,
2459
+ ) as SignatureEnvelope.KeychainRpc
2460
+
2461
+ expect(rpc.keyId).toBeUndefined()
2462
+ })
2348
2463
  })
2349
2464
  })
2350
2465
 
@@ -22,6 +22,7 @@ import * as ox_WebAuthnP256 from '../core/WebAuthnP256.js'
22
22
  const serializedP256Type = '0x01'
23
23
  const serializedWebAuthnType = '0x02'
24
24
  const serializedKeychainType = '0x03'
25
+ const serializedKeychainV2Type = '0x04'
25
26
 
26
27
  /** Serialized magic identifier for Tempo signature envelopes. */
27
28
  export const magicBytes =
@@ -86,9 +87,10 @@ export type GetType<
86
87
  * and clientDataJSON. Enables browser passkey authentication. The signature is also
87
88
  * charged as calldata (16 gas/non-zero byte, 4 gas/zero byte).
88
89
  *
89
- * - **keychain** (type `0x03`): Access key signature that wraps an inner signature (secp256k1,
90
- * p256, or webAuthn). Format: `0x03` + user_address (20 bytes) + inner signature. The
91
- * protocol validates the access key authorization via the AccountKeychain precompile.
90
+ * - **keychain** (type `0x03` V1, `0x04` V2): Access key signature that wraps an inner signature
91
+ * (secp256k1, p256, or webAuthn). Format: type byte + user_address (20 bytes) + inner signature.
92
+ * V2 binds the signature to the user account via `keccak256(sigHash || userAddress)`.
93
+ * The protocol validates the access key authorization via the AccountKeychain precompile.
92
94
  *
93
95
  * [Signature Types Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types)
94
96
  */
@@ -106,18 +108,33 @@ export type SignatureEnvelopeRpc = OneOf<
106
108
  Secp256k1Rpc | P256Rpc | WebAuthnRpc | KeychainRpc
107
109
  >
108
110
 
111
+ /**
112
+ * Keychain signature version.
113
+ *
114
+ * - `'v1'`: Legacy format. Inner signature signs the raw `sig_hash` directly. Deprecated at T1C.
115
+ * - `'v2'`: Inner signature signs `keccak256(sig_hash || user_address)`, binding the signature
116
+ * to the specific user account.
117
+ */
118
+ export type KeychainVersion = 'v1' | 'v2'
119
+
109
120
  export type Keychain<bigintType = bigint, numberType = number> = {
110
121
  /** Root account address that this transaction is being executed for */
111
122
  userAddress: Address.Address
112
123
  /** The actual signature from the access key (can be Secp256k1, P256, or WebAuthn) */
113
124
  inner: SignatureEnvelope<bigintType, numberType>
125
+ /** The access key address (recovered address of the access key signer). */
126
+ keyId?: Address.Address | undefined
114
127
  type: 'keychain'
128
+ /** Keychain signature version. @default 'v1' */
129
+ version?: KeychainVersion | undefined
115
130
  }
116
131
 
117
132
  export type KeychainRpc = {
118
133
  type: 'keychain'
119
134
  userAddress: Address.Address
135
+ keyId?: Address.Address | undefined
120
136
  signature: SignatureEnvelopeRpc
137
+ version?: KeychainVersion | undefined
121
138
  }
122
139
 
123
140
  export type P256<bigintType = bigint, numberType = number> = {
@@ -389,7 +406,8 @@ export declare namespace extractPublicKey {
389
406
  * - 65 bytes (no prefix): secp256k1 signature
390
407
  * - Type `0x01` + 129 bytes: P256 signature (r, s, pubKeyX, pubKeyY, prehash)
391
408
  * - Type `0x02` + variable: WebAuthn signature (webauthnData, r, s, pubKeyX, pubKeyY)
392
- * - Type `0x03` + 20 bytes + inner: Keychain signature (userAddress + inner signature)
409
+ * - Type `0x03` + 20 bytes + inner: Keychain V1 signature (userAddress + inner signature)
410
+ * - Type `0x04` + 20 bytes + inner: Keychain V2 signature (userAddress + inner signature)
393
411
  *
394
412
  * [Signature Types](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types)
395
413
  *
@@ -510,7 +528,10 @@ export function deserialize(value: Serialized): SignatureEnvelope {
510
528
  } satisfies WebAuthn
511
529
  }
512
530
 
513
- if (typeId === serializedKeychainType) {
531
+ if (
532
+ typeId === serializedKeychainType ||
533
+ typeId === serializedKeychainV2Type
534
+ ) {
514
535
  const userAddress = Hex.slice(data, 0, 20)
515
536
  const inner = deserialize(Hex.slice(data, 20))
516
537
 
@@ -518,11 +539,12 @@ export function deserialize(value: Serialized): SignatureEnvelope {
518
539
  userAddress,
519
540
  inner,
520
541
  type: 'keychain',
542
+ version: typeId === serializedKeychainV2Type ? 'v2' : 'v1',
521
543
  } satisfies Keychain
522
544
  }
523
545
 
524
546
  throw new InvalidSerializedError({
525
- reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256) or ${serializedWebAuthnType} (WebAuthn)`,
547
+ reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256), ${serializedWebAuthnType} (WebAuthn), ${serializedKeychainType} (Keychain V1), or ${serializedKeychainV2Type} (Keychain V2)`,
526
548
  serialized,
527
549
  })
528
550
  }
@@ -644,6 +666,7 @@ export function deserialize(value: Serialized): SignatureEnvelope {
644
666
  */
645
667
  export function from<const value extends from.Value>(
646
668
  value: value | from.Value,
669
+ options?: from.Options | undefined,
647
670
  ): from.ReturnValue<value> {
648
671
  if (typeof value === 'string') return deserialize(value) as never
649
672
 
@@ -660,11 +683,45 @@ export function from<const value extends from.Value>(
660
683
  return {
661
684
  ...value,
662
685
  ...(type === 'p256' ? { prehash: value.prehash } : {}),
686
+ ...(type === 'keychain'
687
+ ? {
688
+ ...(!(
689
+ typeof value === 'object' &&
690
+ value !== null &&
691
+ 'version' in value &&
692
+ value.version
693
+ )
694
+ ? { version: 'v2' }
695
+ : {}),
696
+ ...(!(typeof value === 'object' && 'keyId' in value && value.keyId)
697
+ ? (() => {
698
+ const inner = (value as Keychain).inner
699
+ if (inner.type === 'p256' || inner.type === 'webAuthn')
700
+ return { keyId: Address.fromPublicKey(inner.publicKey) }
701
+ if (inner.type === 'secp256k1' && options?.payload)
702
+ return {
703
+ keyId: Address.fromPublicKey(
704
+ ox_Secp256k1.recoverPublicKey({
705
+ payload: options.payload,
706
+ signature: inner.signature,
707
+ }),
708
+ ),
709
+ }
710
+ return {}
711
+ })()
712
+ : {}),
713
+ }
714
+ : {}),
663
715
  type,
664
716
  } as never
665
717
  }
666
718
 
667
719
  export declare namespace from {
720
+ type Options = {
721
+ /** Payload that was signed. Used to recover `keyId` for keychain envelopes with secp256k1 inner signatures. */
722
+ payload?: Hex.Hex | Bytes.Bytes | undefined
723
+ }
724
+
668
725
  type Value =
669
726
  | UnionPartialBy<SignatureEnvelope, 'prehash' | 'type'>
670
727
  | Secp256k1Flat
@@ -678,7 +735,14 @@ export declare namespace from {
678
735
  ? Secp256k1
679
736
  : IsNarrowable<value, SignatureEnvelope> extends true
680
737
  ? SignatureEnvelope
681
- : Assign<value, { readonly type: GetType<value> }>
738
+ : Assign<
739
+ value,
740
+ {
741
+ readonly type: GetType<value>
742
+ } & (GetType<value> extends 'keychain'
743
+ ? { keyId?: Address.Address | undefined }
744
+ : {})
745
+ >
682
746
  >
683
747
  >
684
748
  }
@@ -778,6 +842,8 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
778
842
  type: 'keychain',
779
843
  userAddress: envelope.userAddress,
780
844
  inner: fromRpc(envelope.signature),
845
+ ...(envelope.keyId ? { keyId: envelope.keyId } : {}),
846
+ ...(envelope.version ? { version: envelope.version } : {}),
781
847
  }
782
848
 
783
849
  throw new CoercionError({ envelope })
@@ -868,7 +934,8 @@ export function getType<
868
934
  * - secp256k1: 65 bytes (no type prefix, for backward compatibility)
869
935
  * - P256: `0x01` + r (32) + s (32) + pubKeyX (32) + pubKeyY (32) + prehash (1) = 130 bytes
870
936
  * - WebAuthn: `0x02` + webauthnData (variable) + r (32) + s (32) + pubKeyX (32) + pubKeyY (32)
871
- * - Keychain: `0x03` + userAddress (20) + inner signature (recursive)
937
+ * - Keychain V1: `0x03` + userAddress (20) + inner signature (recursive)
938
+ * - Keychain V2: `0x04` + userAddress (20) + inner signature (recursive)
872
939
  *
873
940
  * [Signature Types](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types)
874
941
  *
@@ -936,8 +1003,12 @@ export function serialize(
936
1003
 
937
1004
  if (type === 'keychain') {
938
1005
  const keychain = envelope as Keychain
1006
+ const keychainTypeId =
1007
+ keychain.version === 'v1'
1008
+ ? serializedKeychainType
1009
+ : serializedKeychainV2Type
939
1010
  return Hex.concat(
940
- serializedKeychainType,
1011
+ keychainTypeId,
941
1012
  keychain.userAddress,
942
1013
  serialize(keychain.inner),
943
1014
  options.magic ? magicBytes : '0x',
@@ -1019,6 +1090,8 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1019
1090
  type: 'keychain',
1020
1091
  userAddress: keychain.userAddress,
1021
1092
  signature: toRpc(keychain.inner),
1093
+ ...(keychain.keyId ? { keyId: keychain.keyId } : {}),
1094
+ ...(keychain.version ? { version: keychain.version } : {}),
1022
1095
  }
1023
1096
  }
1024
1097