ox 0.13.1 → 0.14.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 (79) hide show
  1. package/Base32/package.json +6 -0
  2. package/Bech32m/package.json +6 -0
  3. package/CHANGELOG.md +25 -0
  4. package/CompactSize/package.json +6 -0
  5. package/_cjs/core/Base32.js +73 -0
  6. package/_cjs/core/Base32.js.map +1 -0
  7. package/_cjs/core/Bech32m.js +205 -0
  8. package/_cjs/core/Bech32m.js.map +1 -0
  9. package/_cjs/core/CompactSize.js +91 -0
  10. package/_cjs/core/CompactSize.js.map +1 -0
  11. package/_cjs/index.js +5 -2
  12. package/_cjs/index.js.map +1 -1
  13. package/_cjs/tempo/KeyAuthorization.js +4 -4
  14. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  15. package/_cjs/tempo/SignatureEnvelope.js +18 -3
  16. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  17. package/_cjs/tempo/TempoAddress.js +117 -0
  18. package/_cjs/tempo/TempoAddress.js.map +1 -0
  19. package/_cjs/tempo/TxEnvelopeTempo.js +5 -2
  20. package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
  21. package/_cjs/tempo/index.js +2 -1
  22. package/_cjs/tempo/index.js.map +1 -1
  23. package/_cjs/version.js +1 -1
  24. package/_esm/core/Base32.js +119 -0
  25. package/_esm/core/Base32.js.map +1 -0
  26. package/_esm/core/Bech32m.js +238 -0
  27. package/_esm/core/Bech32m.js.map +1 -0
  28. package/_esm/core/CompactSize.js +150 -0
  29. package/_esm/core/CompactSize.js.map +1 -0
  30. package/_esm/index.js +72 -0
  31. package/_esm/index.js.map +1 -1
  32. package/_esm/tempo/KeyAuthorization.js +19 -9
  33. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  34. package/_esm/tempo/SignatureEnvelope.js +22 -5
  35. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  36. package/_esm/tempo/TempoAddress.js +182 -0
  37. package/_esm/tempo/TempoAddress.js.map +1 -0
  38. package/_esm/tempo/TxEnvelopeTempo.js +42 -2
  39. package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
  40. package/_esm/tempo/index.js +21 -0
  41. package/_esm/tempo/index.js.map +1 -1
  42. package/_esm/version.js +1 -1
  43. package/_types/core/Base32.d.ts +79 -0
  44. package/_types/core/Base32.d.ts.map +1 -0
  45. package/_types/core/Bech32m.d.ts +93 -0
  46. package/_types/core/Bech32m.d.ts.map +1 -0
  47. package/_types/core/CompactSize.d.ts +107 -0
  48. package/_types/core/CompactSize.d.ts.map +1 -0
  49. package/_types/index.d.ts +72 -0
  50. package/_types/index.d.ts.map +1 -1
  51. package/_types/tempo/KeyAuthorization.d.ts +17 -7
  52. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  53. package/_types/tempo/SignatureEnvelope.d.ts +19 -5
  54. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  55. package/_types/tempo/TempoAddress.d.ts +126 -0
  56. package/_types/tempo/TempoAddress.d.ts.map +1 -0
  57. package/_types/tempo/TxEnvelopeTempo.d.ts +47 -1
  58. package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  59. package/_types/tempo/index.d.ts +21 -0
  60. package/_types/tempo/index.d.ts.map +1 -1
  61. package/_types/version.d.ts +1 -1
  62. package/core/Base32.ts +134 -0
  63. package/core/Bech32m.ts +263 -0
  64. package/core/CompactSize.ts +169 -0
  65. package/index.ts +74 -1
  66. package/package.json +21 -1
  67. package/tempo/KeyAuthorization.test.ts +70 -23
  68. package/tempo/KeyAuthorization.ts +21 -18
  69. package/tempo/SignatureEnvelope.test.ts +15 -8
  70. package/tempo/SignatureEnvelope.ts +43 -8
  71. package/tempo/TempoAddress/package.json +6 -0
  72. package/tempo/TempoAddress.test.ts +237 -0
  73. package/tempo/TempoAddress.ts +222 -0
  74. package/tempo/Transaction.test.ts +4 -2
  75. package/tempo/TxEnvelopeTempo.test.ts +7 -3
  76. package/tempo/TxEnvelopeTempo.ts +52 -1
  77. package/tempo/e2e.test.ts +45 -10
  78. package/tempo/index.ts +22 -0
  79. package/version.ts +1 -1
@@ -34,8 +34,8 @@ export type KeyAuthorization<
34
34
  > = {
35
35
  /** Address derived from the public key of the key type. */
36
36
  address: Address.Address
37
- /** Chain ID for replay protection (0 = valid on any chain). */
38
- chainId?: bigintType | undefined
37
+ /** Chain ID for replay protection. */
38
+ chainId: bigintType
39
39
  /** Unix timestamp when key expires (0 = never expires). */
40
40
  expiry?: numberType | null | undefined
41
41
  /** TIP20 spending limits for this key. */
@@ -136,6 +136,7 @@ export type TokenLimit<bigintType = bigint> = {
136
136
  *
137
137
  * const authorization = KeyAuthorization.from({
138
138
  * address,
139
+ * chainId: 4217n,
139
140
  * expiry: 1234567890,
140
141
  * type: 'secp256k1',
141
142
  * limits: [{
@@ -157,6 +158,7 @@ export type TokenLimit<bigintType = bigint> = {
157
158
  *
158
159
  * const authorization = KeyAuthorization.from({
159
160
  * address,
161
+ * chainId: 4217n,
160
162
  * expiry: 1234567890,
161
163
  * type: 'p256',
162
164
  * limits: [{
@@ -181,6 +183,7 @@ export type TokenLimit<bigintType = bigint> = {
181
183
  *
182
184
  * const authorization = KeyAuthorization.from({
183
185
  * address,
186
+ * chainId: 4217n,
184
187
  * expiry: 1234567890,
185
188
  * type: 'secp256k1',
186
189
  * limits: [{
@@ -214,6 +217,7 @@ export type TokenLimit<bigintType = bigint> = {
214
217
  *
215
218
  * const authorization = KeyAuthorization.from({
216
219
  * address,
220
+ * chainId: 4217n,
217
221
  * expiry: 1234567890,
218
222
  * type: 'p256',
219
223
  * limits: [{
@@ -313,7 +317,7 @@ export declare namespace from {
313
317
  * @returns A signed {@link ox#AuthorizationTempo.AuthorizationTempo}.
314
318
  */
315
319
  export function fromRpc(authorization: Rpc): Signed {
316
- const { chainId = '0x0', keyId, expiry = 0, limits, keyType } = authorization
320
+ const { chainId, keyId, expiry = 0, limits, keyType } = authorization
317
321
  const signature = SignatureEnvelope.fromRpc(authorization.signature)
318
322
  return {
319
323
  address: keyId,
@@ -393,7 +397,7 @@ export function fromTuple<const tuple extends Tuple>(
393
397
  address: keyId,
394
398
  expiry: typeof expiry !== 'undefined' ? hexToNumber(expiry) : undefined,
395
399
  type: keyType,
396
- ...(chainId !== '0x' ? { chainId: Hex.toBigInt(chainId) } : {}),
400
+ chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
397
401
  ...(typeof expiry !== 'undefined' ? { expiry: hexToNumber(expiry) } : {}),
398
402
  ...(typeof limits !== 'undefined'
399
403
  ? {
@@ -436,6 +440,7 @@ export declare namespace fromTuple {
436
440
  *
437
441
  * const authorization = KeyAuthorization.from({
438
442
  * address,
443
+ * chainId: 4217n,
439
444
  * expiry: 1234567890,
440
445
  * type: 'secp256k1',
441
446
  * limits: [{
@@ -467,8 +472,9 @@ export declare namespace getSignPayload {
467
472
  * import { Value } from 'ox'
468
473
  *
469
474
  * const authorization = KeyAuthorization.from({
470
- * expiry: 1234567890,
471
475
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
476
+ * chainId: 4217n,
477
+ * expiry: 1234567890,
472
478
  * type: 'secp256k1',
473
479
  * limits: [{
474
480
  * token: '0x20c0000000000000000000000000000000000001',
@@ -504,8 +510,9 @@ export declare namespace deserialize {
504
510
  * import { Value } from 'ox'
505
511
  *
506
512
  * const authorization = KeyAuthorization.from({
507
- * expiry: 1234567890,
508
513
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
514
+ * chainId: 4217n,
515
+ * expiry: 1234567890,
509
516
  * type: 'secp256k1',
510
517
  * limits: [{
511
518
  * token: '0x20c0000000000000000000000000000000000001',
@@ -543,8 +550,9 @@ export declare namespace hash {
543
550
  * import { Value } from 'ox'
544
551
  *
545
552
  * const authorization = KeyAuthorization.from({
546
- * expiry: 1234567890,
547
553
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
554
+ * chainId: 4217n,
555
+ * expiry: 1234567890,
548
556
  * type: 'secp256k1',
549
557
  * limits: [{
550
558
  * token: '0x20c0000000000000000000000000000000000001',
@@ -579,8 +587,9 @@ export declare namespace serialize {
579
587
  * import { Value } from 'ox'
580
588
  *
581
589
  * const authorization = KeyAuthorization.toRpc({
582
- * expiry: 1234567890,
583
590
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
591
+ * chainId: 4217n,
592
+ * expiry: 1234567890,
584
593
  * type: 'secp256k1',
585
594
  * limits: [{
586
595
  * token: '0x20c0000000000000000000000000000000000001',
@@ -601,14 +610,7 @@ export declare namespace serialize {
601
610
  * @returns An RPC-formatted Key Authorization.
602
611
  */
603
612
  export function toRpc(authorization: Signed): Rpc {
604
- const {
605
- address,
606
- chainId = 0n,
607
- expiry,
608
- limits,
609
- type,
610
- signature,
611
- } = authorization
613
+ const { address, chainId, expiry, limits, type, signature } = authorization
612
614
 
613
615
  return {
614
616
  chainId: chainId === 0n ? '0x' : Hex.fromNumber(chainId),
@@ -636,8 +638,9 @@ export declare namespace toRpc {
636
638
  * import { Value } from 'ox'
637
639
  *
638
640
  * const authorization = KeyAuthorization.from({
639
- * expiry: 1234567890,
640
641
  * address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
642
+ * chainId: 4217n,
643
+ * expiry: 1234567890,
641
644
  * type: 'secp256k1',
642
645
  * limits: [{
643
646
  * token: '0x20c0000000000000000000000000000000000001',
@@ -660,7 +663,7 @@ export declare namespace toRpc {
660
663
  export function toTuple<const authorization extends KeyAuthorization>(
661
664
  authorization: authorization,
662
665
  ): toTuple.ReturnType<authorization> {
663
- const { address, chainId = 0n, expiry, limits } = authorization
666
+ const { address, chainId, expiry, limits } = authorization
664
667
  const signature = authorization.signature
665
668
  ? SignatureEnvelope.serialize(authorization.signature)
666
669
  : undefined
@@ -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
  })
@@ -1250,8 +1255,8 @@ describe('serialize', () => {
1250
1255
  // Should be: 1 (type) + 20 (address) + 65 (secp256k1 signature)
1251
1256
  expect(Hex.size(serialized)).toBe(1 + 20 + 65)
1252
1257
 
1253
- // First byte should be Keychain type identifier (0x03)
1254
- expect(Hex.slice(serialized, 0, 1)).toBe('0x03')
1258
+ // First byte should be Keychain V2 type identifier (0x04)
1259
+ expect(Hex.slice(serialized, 0, 1)).toBe('0x04')
1255
1260
 
1256
1261
  // Next 20 bytes should be the user address (without '0x')
1257
1262
  expect(Hex.slice(serialized, 1, 21)).toBe(
@@ -1265,8 +1270,8 @@ describe('serialize', () => {
1265
1270
  // Should be: 1 (type) + 20 (address) + 130 (p256 signature with type)
1266
1271
  expect(Hex.size(serialized)).toBe(1 + 20 + 130)
1267
1272
 
1268
- // First byte should be Keychain type identifier (0x03)
1269
- expect(Hex.slice(serialized, 0, 1)).toBe('0x03')
1273
+ // First byte should be Keychain V2 type identifier (0x04)
1274
+ expect(Hex.slice(serialized, 0, 1)).toBe('0x04')
1270
1275
 
1271
1276
  // Next 20 bytes should be the user address (without '0x')
1272
1277
  expect(Hex.slice(serialized, 1, 21)).toBe(
@@ -1284,8 +1289,8 @@ describe('serialize', () => {
1284
1289
  signature_keychain_webauthn,
1285
1290
  )
1286
1291
 
1287
- // First byte should be Keychain type identifier (0x03)
1288
- expect(Hex.slice(serialized, 0, 1)).toBe('0x03')
1292
+ // First byte should be Keychain V2 type identifier (0x04)
1293
+ expect(Hex.slice(serialized, 0, 1)).toBe('0x04')
1289
1294
 
1290
1295
  // Should contain the user address
1291
1296
  expect(Hex.slice(serialized, 1, 21)).toBe(
@@ -1519,6 +1524,7 @@ describe('serialize', () => {
1519
1524
  },
1520
1525
  "type": "keychain",
1521
1526
  "userAddress": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
1527
+ "version": "v2",
1522
1528
  }
1523
1529
  `)
1524
1530
  })
@@ -1549,6 +1555,7 @@ describe('serialize', () => {
1549
1555
  },
1550
1556
  "type": "keychain",
1551
1557
  "userAddress": "0xfedcbafedcbafedcbafedcbafedcbafedcbafedc",
1558
+ "version": "v2",
1552
1559
  }
1553
1560
  `)
1554
1561
  })
@@ -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,30 @@ 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>
114
125
  type: 'keychain'
126
+ /** Keychain signature version. @default 'v1' */
127
+ version?: KeychainVersion | undefined
115
128
  }
116
129
 
117
130
  export type KeychainRpc = {
118
131
  type: 'keychain'
119
132
  userAddress: Address.Address
120
133
  signature: SignatureEnvelopeRpc
134
+ version?: KeychainVersion | undefined
121
135
  }
122
136
 
123
137
  export type P256<bigintType = bigint, numberType = number> = {
@@ -389,7 +403,8 @@ export declare namespace extractPublicKey {
389
403
  * - 65 bytes (no prefix): secp256k1 signature
390
404
  * - Type `0x01` + 129 bytes: P256 signature (r, s, pubKeyX, pubKeyY, prehash)
391
405
  * - Type `0x02` + variable: WebAuthn signature (webauthnData, r, s, pubKeyX, pubKeyY)
392
- * - Type `0x03` + 20 bytes + inner: Keychain signature (userAddress + inner signature)
406
+ * - Type `0x03` + 20 bytes + inner: Keychain V1 signature (userAddress + inner signature)
407
+ * - Type `0x04` + 20 bytes + inner: Keychain V2 signature (userAddress + inner signature)
393
408
  *
394
409
  * [Signature Types](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types)
395
410
  *
@@ -510,7 +525,10 @@ export function deserialize(value: Serialized): SignatureEnvelope {
510
525
  } satisfies WebAuthn
511
526
  }
512
527
 
513
- if (typeId === serializedKeychainType) {
528
+ if (
529
+ typeId === serializedKeychainType ||
530
+ typeId === serializedKeychainV2Type
531
+ ) {
514
532
  const userAddress = Hex.slice(data, 0, 20)
515
533
  const inner = deserialize(Hex.slice(data, 20))
516
534
 
@@ -518,11 +536,12 @@ export function deserialize(value: Serialized): SignatureEnvelope {
518
536
  userAddress,
519
537
  inner,
520
538
  type: 'keychain',
539
+ version: typeId === serializedKeychainV2Type ? 'v2' : 'v1',
521
540
  } satisfies Keychain
522
541
  }
523
542
 
524
543
  throw new InvalidSerializedError({
525
- reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256) or ${serializedWebAuthnType} (WebAuthn)`,
544
+ reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256), ${serializedWebAuthnType} (WebAuthn), ${serializedKeychainType} (Keychain V1), or ${serializedKeychainV2Type} (Keychain V2)`,
526
545
  serialized,
527
546
  })
528
547
  }
@@ -660,6 +679,15 @@ export function from<const value extends from.Value>(
660
679
  return {
661
680
  ...value,
662
681
  ...(type === 'p256' ? { prehash: value.prehash } : {}),
682
+ ...(type === 'keychain' &&
683
+ !(
684
+ typeof value === 'object' &&
685
+ value !== null &&
686
+ 'version' in value &&
687
+ value.version
688
+ )
689
+ ? { version: 'v1' }
690
+ : {}),
663
691
  type,
664
692
  } as never
665
693
  }
@@ -778,6 +806,7 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
778
806
  type: 'keychain',
779
807
  userAddress: envelope.userAddress,
780
808
  inner: fromRpc(envelope.signature),
809
+ ...(envelope.version ? { version: envelope.version } : {}),
781
810
  }
782
811
 
783
812
  throw new CoercionError({ envelope })
@@ -868,7 +897,8 @@ export function getType<
868
897
  * - secp256k1: 65 bytes (no type prefix, for backward compatibility)
869
898
  * - P256: `0x01` + r (32) + s (32) + pubKeyX (32) + pubKeyY (32) + prehash (1) = 130 bytes
870
899
  * - WebAuthn: `0x02` + webauthnData (variable) + r (32) + s (32) + pubKeyX (32) + pubKeyY (32)
871
- * - Keychain: `0x03` + userAddress (20) + inner signature (recursive)
900
+ * - Keychain V1: `0x03` + userAddress (20) + inner signature (recursive)
901
+ * - Keychain V2: `0x04` + userAddress (20) + inner signature (recursive)
872
902
  *
873
903
  * [Signature Types](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types)
874
904
  *
@@ -936,8 +966,12 @@ export function serialize(
936
966
 
937
967
  if (type === 'keychain') {
938
968
  const keychain = envelope as Keychain
969
+ const keychainTypeId =
970
+ keychain.version === 'v1'
971
+ ? serializedKeychainType
972
+ : serializedKeychainV2Type
939
973
  return Hex.concat(
940
- serializedKeychainType,
974
+ keychainTypeId,
941
975
  keychain.userAddress,
942
976
  serialize(keychain.inner),
943
977
  options.magic ? magicBytes : '0x',
@@ -1019,6 +1053,7 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1019
1053
  type: 'keychain',
1020
1054
  userAddress: keychain.userAddress,
1021
1055
  signature: toRpc(keychain.inner),
1056
+ ...(keychain.version ? { version: keychain.version } : {}),
1022
1057
  }
1023
1058
  }
1024
1059
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "module",
3
+ "types": "../../_types/tempo/TempoAddress.d.ts",
4
+ "main": "../../_cjs/tempo/TempoAddress.js",
5
+ "module": "../../_esm/tempo/TempoAddress.js"
6
+ }
@@ -0,0 +1,237 @@
1
+ import { Address, Bech32m } from 'ox'
2
+ import { TempoAddress } from 'ox/tempo'
3
+ import { describe, expect, test } from 'vitest'
4
+
5
+ const rawAddress = Address.checksum(
6
+ '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28',
7
+ )
8
+
9
+ describe('format', () => {
10
+ test('mainnet address', () => {
11
+ expect(TempoAddress.format(rawAddress)).toMatchInlineSnapshot(
12
+ `"tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0"`,
13
+ )
14
+ })
15
+
16
+ test('zone address (zone ID = 1)', () => {
17
+ expect(
18
+ TempoAddress.format(rawAddress, { zoneId: 1 }),
19
+ ).toMatchInlineSnapshot(
20
+ `"tempoz1qqqhgtf4e3nrfszn9yj68wzyhj08t90jh55q74d9uj"`,
21
+ )
22
+ })
23
+
24
+ test('zone address (zone ID = 252)', () => {
25
+ expect(
26
+ TempoAddress.format(rawAddress, { zoneId: 252 }),
27
+ ).toMatchInlineSnapshot(
28
+ `"tempoz1qr78gtf4e3nrfszn9yj68wzyhj08t90jh55q9k62jd"`,
29
+ )
30
+ })
31
+
32
+ test('zone address (zone ID = 253)', () => {
33
+ expect(
34
+ TempoAddress.format(rawAddress, { zoneId: 253 }),
35
+ ).toMatchInlineSnapshot(
36
+ `"tempoz1qr7l6qr5956uce35cpfjjfdrhpzte8n4jhet62q0j8hus"`,
37
+ )
38
+ })
39
+
40
+ test('zone address (zone ID = 65535)', () => {
41
+ expect(
42
+ TempoAddress.format(rawAddress, { zoneId: 65535 }),
43
+ ).toMatchInlineSnapshot(
44
+ `"tempoz1qr7lllm5956uce35cpfjjfdrhpzte8n4jhet62q8pdj6j"`,
45
+ )
46
+ })
47
+
48
+ test('zone address (zone ID = 65536)', () => {
49
+ expect(
50
+ TempoAddress.format(rawAddress, { zoneId: 65536 }),
51
+ ).toMatchInlineSnapshot(
52
+ `"tempoz1qrlqqqqpqp6z6dwvvc6vq5efyk3ms39une6etu4a9qdupk5c"`,
53
+ )
54
+ })
55
+
56
+ test('zone address (zone ID = 4294967295)', () => {
57
+ expect(
58
+ TempoAddress.format(rawAddress, { zoneId: 4294967295 }),
59
+ ).toMatchInlineSnapshot(
60
+ `"tempoz1qrl0llllla6z6dwvvc6vq5efyk3ms39une6etu4a9qnk36qy"`,
61
+ )
62
+ })
63
+
64
+ test('zone address (zone ID > 4294967295)', () => {
65
+ expect(
66
+ TempoAddress.format(rawAddress, { zoneId: BigInt('4294967296') }),
67
+ ).toMatchInlineSnapshot(
68
+ `"tempoz1qrlsqqqqqqqsqqqqwskntnrxxnq9x2f95wuyf0y7wk2l90fg4306kk"`,
69
+ )
70
+ })
71
+
72
+ test('lowercase output', () => {
73
+ const result = TempoAddress.format(rawAddress)
74
+ expect(result).toBe(result.toLowerCase())
75
+ })
76
+
77
+ test('spec test vectors', () => {
78
+ expect(TempoAddress.format(rawAddress)).toBe(
79
+ 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0',
80
+ )
81
+ expect(TempoAddress.format(rawAddress, { zoneId: 1 })).toBe(
82
+ 'tempoz1qqqhgtf4e3nrfszn9yj68wzyhj08t90jh55q74d9uj',
83
+ )
84
+ expect(TempoAddress.format(rawAddress, { zoneId: 1000 })).toBe(
85
+ 'tempoz1qr77sqm5956uce35cpfjjfdrhpzte8n4jhet62qxx4zvx',
86
+ )
87
+ expect(TempoAddress.format(rawAddress, { zoneId: 100000 })).toBe(
88
+ 'tempoz1qrl2ppspqp6z6dwvvc6vq5efyk3ms39une6etu4a9qg5477g',
89
+ )
90
+ })
91
+
92
+ test('address lengths match spec', () => {
93
+ expect(TempoAddress.format(rawAddress).length).toBe(46)
94
+ expect(TempoAddress.format(rawAddress, { zoneId: 1 }).length).toBe(49)
95
+ expect(TempoAddress.format(rawAddress, { zoneId: 1000 }).length).toBe(52)
96
+ expect(TempoAddress.format(rawAddress, { zoneId: 100000 }).length).toBe(55)
97
+ })
98
+ })
99
+
100
+ describe('parse', () => {
101
+ test('mainnet', () => {
102
+ const encoded = TempoAddress.format(rawAddress)
103
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
104
+ {
105
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
106
+ "zoneId": undefined,
107
+ }
108
+ `)
109
+ })
110
+
111
+ test('zone (zone ID = 1)', () => {
112
+ const encoded = TempoAddress.format(rawAddress, { zoneId: 1 })
113
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
114
+ {
115
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
116
+ "zoneId": 1n,
117
+ }
118
+ `)
119
+ })
120
+
121
+ test('zone (zone ID = 252)', () => {
122
+ const encoded = TempoAddress.format(rawAddress, { zoneId: 252 })
123
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
124
+ {
125
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
126
+ "zoneId": 252n,
127
+ }
128
+ `)
129
+ })
130
+
131
+ test('zone (zone ID = 253)', () => {
132
+ const encoded = TempoAddress.format(rawAddress, { zoneId: 253 })
133
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
134
+ {
135
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
136
+ "zoneId": 253n,
137
+ }
138
+ `)
139
+ })
140
+
141
+ test('zone (zone ID = 65535)', () => {
142
+ const encoded = TempoAddress.format(rawAddress, { zoneId: 65535 })
143
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
144
+ {
145
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
146
+ "zoneId": 65535n,
147
+ }
148
+ `)
149
+ })
150
+
151
+ test('zone (zone ID = 65536)', () => {
152
+ const encoded = TempoAddress.format(rawAddress, { zoneId: 65536 })
153
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
154
+ {
155
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
156
+ "zoneId": 65536n,
157
+ }
158
+ `)
159
+ })
160
+
161
+ test('zone (large zone ID)', () => {
162
+ const encoded = TempoAddress.format(rawAddress, {
163
+ zoneId: BigInt('18446744073709551615'),
164
+ })
165
+ expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
166
+ {
167
+ "address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
168
+ "zoneId": 18446744073709551615n,
169
+ }
170
+ `)
171
+ })
172
+
173
+ test('all uppercase', () => {
174
+ const encoded = TempoAddress.format(rawAddress)
175
+ const upper = encoded.toUpperCase()
176
+ expect(TempoAddress.parse(upper).address).toBe(rawAddress)
177
+ })
178
+
179
+ test('error: invalid prefix', () => {
180
+ const encoded = Bech32m.encode('bitcoin', new Uint8Array(20))
181
+ expect(() =>
182
+ TempoAddress.parse(encoded),
183
+ ).toThrowErrorMatchingInlineSnapshot(
184
+ `[TempoAddress.InvalidPrefixError: Tempo address "${encoded}" has an invalid prefix. Expected "tempo1" or "tempoz1".]`,
185
+ )
186
+ })
187
+
188
+ test('error: unsupported version', () => {
189
+ const data = new Uint8Array([0x01, ...new Uint8Array(20)])
190
+ const encoded = Bech32m.encode('tempo', data)
191
+ expect(() => TempoAddress.parse(encoded)).toThrow(
192
+ TempoAddress.InvalidVersionError,
193
+ )
194
+ })
195
+
196
+ test('error: invalid checksum', () => {
197
+ const encoded = TempoAddress.format(rawAddress)
198
+ const tampered = encoded.slice(0, -1) + (encoded.endsWith('q') ? 'p' : 'q')
199
+ expect(() =>
200
+ TempoAddress.parse(tampered),
201
+ ).toThrowErrorMatchingInlineSnapshot(
202
+ `[TempoAddress.InvalidChecksumError: Tempo address "${tampered}" has an invalid checksum.]`,
203
+ )
204
+ })
205
+
206
+ test('error: prefix swap detected', () => {
207
+ const mainnet = TempoAddress.format(rawAddress)
208
+ const swapped = 'tempoz1' + mainnet.slice(6)
209
+ expect(() =>
210
+ TempoAddress.parse(swapped),
211
+ ).toThrowErrorMatchingInlineSnapshot(
212
+ `[TempoAddress.InvalidChecksumError: Tempo address "tempoz1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0" has an invalid checksum.]`,
213
+ )
214
+ })
215
+ })
216
+
217
+ describe('validate', () => {
218
+ test('valid mainnet address', () => {
219
+ const encoded = TempoAddress.format(rawAddress)
220
+ expect(TempoAddress.validate(encoded)).toMatchInlineSnapshot(`true`)
221
+ })
222
+
223
+ test('valid zone address', () => {
224
+ const encoded = TempoAddress.format(rawAddress, { zoneId: 1 })
225
+ expect(TempoAddress.validate(encoded)).toMatchInlineSnapshot(`true`)
226
+ })
227
+
228
+ test('invalid address', () => {
229
+ expect(TempoAddress.validate('invalid')).toMatchInlineSnapshot(`false`)
230
+ })
231
+
232
+ test('tampered address', () => {
233
+ const encoded = TempoAddress.format(rawAddress)
234
+ const tampered = encoded.slice(0, -1) + (encoded.endsWith('q') ? 'p' : 'q')
235
+ expect(TempoAddress.validate(tampered)).toMatchInlineSnapshot(`false`)
236
+ })
237
+ })