ox 0.13.2 → 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 (61) hide show
  1. package/Bech32m/package.json +6 -0
  2. package/CHANGELOG.md +19 -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/KeyAuthorization.js +4 -4
  8. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  9. package/_cjs/tempo/SignatureEnvelope.js +18 -3
  10. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  11. package/_cjs/tempo/TempoAddress.js +40 -39
  12. package/_cjs/tempo/TempoAddress.js.map +1 -1
  13. package/_cjs/tempo/TxEnvelopeTempo.js +5 -2
  14. package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
  15. package/_cjs/tempo/index.js.map +1 -1
  16. package/_cjs/version.js +1 -1
  17. package/_esm/core/Bech32m.js +238 -0
  18. package/_esm/core/Bech32m.js.map +1 -0
  19. package/_esm/index.js +24 -0
  20. package/_esm/index.js.map +1 -1
  21. package/_esm/tempo/KeyAuthorization.js +19 -9
  22. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  23. package/_esm/tempo/SignatureEnvelope.js +22 -5
  24. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  25. package/_esm/tempo/TempoAddress.js +47 -46
  26. package/_esm/tempo/TempoAddress.js.map +1 -1
  27. package/_esm/tempo/TxEnvelopeTempo.js +42 -2
  28. package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
  29. package/_esm/tempo/index.js +2 -2
  30. package/_esm/tempo/index.js.map +1 -1
  31. package/_esm/version.js +1 -1
  32. package/_types/core/Bech32m.d.ts +93 -0
  33. package/_types/core/Bech32m.d.ts.map +1 -0
  34. package/_types/index.d.ts +24 -0
  35. package/_types/index.d.ts.map +1 -1
  36. package/_types/tempo/KeyAuthorization.d.ts +17 -7
  37. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  38. package/_types/tempo/SignatureEnvelope.d.ts +19 -5
  39. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  40. package/_types/tempo/TempoAddress.d.ts +19 -11
  41. package/_types/tempo/TempoAddress.d.ts.map +1 -1
  42. package/_types/tempo/TxEnvelopeTempo.d.ts +47 -1
  43. package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  44. package/_types/tempo/index.d.ts +2 -2
  45. package/_types/tempo/index.d.ts.map +1 -1
  46. package/_types/version.d.ts +1 -1
  47. package/core/Bech32m.ts +263 -0
  48. package/index.ts +24 -2
  49. package/package.json +6 -1
  50. package/tempo/KeyAuthorization.test.ts +70 -23
  51. package/tempo/KeyAuthorization.ts +21 -18
  52. package/tempo/SignatureEnvelope.test.ts +15 -8
  53. package/tempo/SignatureEnvelope.ts +43 -8
  54. package/tempo/TempoAddress.test.ts +49 -14
  55. package/tempo/TempoAddress.ts +56 -59
  56. package/tempo/Transaction.test.ts +4 -2
  57. package/tempo/TxEnvelopeTempo.test.ts +7 -3
  58. package/tempo/TxEnvelopeTempo.ts +52 -1
  59. package/tempo/e2e.test.ts +45 -10
  60. package/tempo/index.ts +6 -2
  61. package/version.ts +1 -1
@@ -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
 
@@ -1,4 +1,4 @@
1
- import { Address } from 'ox'
1
+ import { Address, Bech32m } from 'ox'
2
2
  import { TempoAddress } from 'ox/tempo'
3
3
  import { describe, expect, test } from 'vitest'
4
4
 
@@ -9,27 +9,31 @@ const rawAddress = Address.checksum(
9
9
  describe('format', () => {
10
10
  test('mainnet address', () => {
11
11
  expect(TempoAddress.format(rawAddress)).toMatchInlineSnapshot(
12
- `"tempo1wskntnrxxnq9x2f95wuyf0y7wk2l90fg8zd8djs"`,
12
+ `"tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0"`,
13
13
  )
14
14
  })
15
15
 
16
16
  test('zone address (zone ID = 1)', () => {
17
17
  expect(
18
18
  TempoAddress.format(rawAddress, { zoneId: 1 }),
19
- ).toMatchInlineSnapshot(`"tempoz1q96z6dwvvc6vq5efyk3ms39une6etu4a9zeqtx3q"`)
19
+ ).toMatchInlineSnapshot(
20
+ `"tempoz1qqqhgtf4e3nrfszn9yj68wzyhj08t90jh55q74d9uj"`,
21
+ )
20
22
  })
21
23
 
22
24
  test('zone address (zone ID = 252)', () => {
23
25
  expect(
24
26
  TempoAddress.format(rawAddress, { zoneId: 252 }),
25
- ).toMatchInlineSnapshot(`"tempoz1l36z6dwvvc6vq5efyk3ms39une6etu4a9z8vgw44"`)
27
+ ).toMatchInlineSnapshot(
28
+ `"tempoz1qr78gtf4e3nrfszn9yj68wzyhj08t90jh55q9k62jd"`,
29
+ )
26
30
  })
27
31
 
28
32
  test('zone address (zone ID = 253)', () => {
29
33
  expect(
30
34
  TempoAddress.format(rawAddress, { zoneId: 253 }),
31
35
  ).toMatchInlineSnapshot(
32
- `"tempoz1lh7sqapdxhxxvdxq2v5jtgacgj7fuav4727jsx0032us"`,
36
+ `"tempoz1qr7l6qr5956uce35cpfjjfdrhpzte8n4jhet62q0j8hus"`,
33
37
  )
34
38
  })
35
39
 
@@ -37,7 +41,7 @@ describe('format', () => {
37
41
  expect(
38
42
  TempoAddress.format(rawAddress, { zoneId: 65535 }),
39
43
  ).toMatchInlineSnapshot(
40
- `"tempoz1lhll7apdxhxxvdxq2v5jtgacgj7fuav4727j37cldu7q"`,
44
+ `"tempoz1qr7lllm5956uce35cpfjjfdrhpzte8n4jhet62q8pdj6j"`,
41
45
  )
42
46
  })
43
47
 
@@ -45,7 +49,7 @@ describe('format', () => {
45
49
  expect(
46
50
  TempoAddress.format(rawAddress, { zoneId: 65536 }),
47
51
  ).toMatchInlineSnapshot(
48
- `"tempoz1lcqqqqgqwskntnrxxnq9x2f95wuyf0y7wk2l90fga965qjc"`,
52
+ `"tempoz1qrlqqqqpqp6z6dwvvc6vq5efyk3ms39une6etu4a9qdupk5c"`,
49
53
  )
50
54
  })
51
55
 
@@ -53,7 +57,7 @@ describe('format', () => {
53
57
  expect(
54
58
  TempoAddress.format(rawAddress, { zoneId: 4294967295 }),
55
59
  ).toMatchInlineSnapshot(
56
- `"tempoz1lmllllllwskntnrxxnq9x2f95wuyf0y7wk2l90fgxg58ulq"`,
60
+ `"tempoz1qrl0llllla6z6dwvvc6vq5efyk3ms39une6etu4a9qnk36qy"`,
57
61
  )
58
62
  })
59
63
 
@@ -61,7 +65,7 @@ describe('format', () => {
61
65
  expect(
62
66
  TempoAddress.format(rawAddress, { zoneId: BigInt('4294967296') }),
63
67
  ).toMatchInlineSnapshot(
64
- `"tempoz1luqqqqqqqyqqqqr5956uce35cpfjjfdrhpzte8n4jhet62pnyj7cc"`,
68
+ `"tempoz1qrlsqqqqqqqsqqqqwskntnrxxnq9x2f95wuyf0y7wk2l90fg4306kk"`,
65
69
  )
66
70
  })
67
71
 
@@ -69,6 +73,28 @@ describe('format', () => {
69
73
  const result = TempoAddress.format(rawAddress)
70
74
  expect(result).toBe(result.toLowerCase())
71
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
+ })
72
98
  })
73
99
 
74
100
  describe('parse', () => {
@@ -144,17 +170,26 @@ describe('parse', () => {
144
170
  `)
145
171
  })
146
172
 
147
- test('case insensitive', () => {
173
+ test('all uppercase', () => {
148
174
  const encoded = TempoAddress.format(rawAddress)
149
- const upper = encoded.slice(0, 6) + encoded.slice(6).toUpperCase()
175
+ const upper = encoded.toUpperCase()
150
176
  expect(TempoAddress.parse(upper).address).toBe(rawAddress)
151
177
  })
152
178
 
153
179
  test('error: invalid prefix', () => {
180
+ const encoded = Bech32m.encode('bitcoin', new Uint8Array(20))
154
181
  expect(() =>
155
- TempoAddress.parse('bitcoin1abc'),
182
+ TempoAddress.parse(encoded),
156
183
  ).toThrowErrorMatchingInlineSnapshot(
157
- `[TempoAddress.InvalidPrefixError: Tempo address "bitcoin1abc" has an invalid prefix. Expected "tempo1" or "tempoz1".]`,
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,
158
193
  )
159
194
  })
160
195
 
@@ -174,7 +209,7 @@ describe('parse', () => {
174
209
  expect(() =>
175
210
  TempoAddress.parse(swapped),
176
211
  ).toThrowErrorMatchingInlineSnapshot(
177
- `[TempoAddress.InvalidLengthError: Tempo address "${swapped}" has an invalid payload length. Expected 24 bytes, got 23.]`,
212
+ `[TempoAddress.InvalidChecksumError: Tempo address "tempoz1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0" has an invalid checksum.]`,
178
213
  )
179
214
  })
180
215
  })
@@ -1,9 +1,8 @@
1
1
  import * as Address from '../core/Address.js'
2
- import * as Base32 from '../core/Base32.js'
2
+ import * as Bech32m from '../core/Bech32m.js'
3
3
  import * as Bytes from '../core/Bytes.js'
4
4
  import * as CompactSize from '../core/CompactSize.js'
5
5
  import * as Errors from '../core/Errors.js'
6
- import * as Hash from '../core/Hash.js'
7
6
  import * as Hex from '../core/Hex.js'
8
7
 
9
8
  /** Root type for a Tempo Address. */
@@ -17,7 +16,7 @@ export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
17
16
  * import { TempoAddress } from 'ox/tempo'
18
17
  *
19
18
  * const address = TempoAddress.format('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28')
20
- * // @log: 'tempo1wskntnrxxnq9x2f95wuyf0y7wk2l90fg8zd8djs'
19
+ * // @log: 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0'
21
20
  * ```
22
21
  *
23
22
  * @example
@@ -29,7 +28,7 @@ export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
29
28
  * '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28',
30
29
  * { zoneId: 1 },
31
30
  * )
32
- * // @log: 'tempoz1q96z6dwvvc6vq5efyk3ms39une6etu4a9zeqtx3q'
31
+ * // @log: 'tempoz1qqqhgtf4e3nrfszn9yj68wzyhj08t90jh55q74d9uj'
33
32
  * ```
34
33
  *
35
34
  * @param address - The raw 20-byte Ethereum address.
@@ -42,23 +41,18 @@ export function format(
42
41
  ): TempoAddress {
43
42
  const { zoneId } = options
44
43
 
45
- const prefix = zoneId != null ? 'tempoz1' : 'tempo1'
44
+ const hrp = zoneId != null ? 'tempoz' : 'tempo'
45
+ const version = new Uint8Array([0x00])
46
46
  const zone = zoneId != null ? CompactSize.toBytes(zoneId) : new Uint8Array()
47
- const address_bytes = Bytes.fromHex(address)
47
+ const data = Bytes.concat(version, zone, Bytes.fromHex(address))
48
48
 
49
- const input = Bytes.concat(Bytes.fromString(prefix), zone, address_bytes)
50
- const checksum = Hash.sha256(Hash.sha256(input, { as: 'Bytes' }), {
51
- as: 'Bytes',
52
- }).slice(0, 4)
53
-
54
- const payload = Bytes.concat(zone, address_bytes, checksum)
55
- return `${prefix}${Base32.fromBytes(payload)}` as TempoAddress
49
+ return Bech32m.encode(hrp, data) as TempoAddress
56
50
  }
57
51
 
58
52
  export declare namespace format {
59
53
  type Options = {
60
54
  /** Zone ID for zone addresses. */
61
- zoneId?: bigint | number | undefined
55
+ zoneId?: number | bigint | undefined
62
56
  }
63
57
 
64
58
  type ErrorType = Errors.GlobalErrorType
@@ -73,9 +67,9 @@ export declare namespace format {
73
67
  * import { TempoAddress } from 'ox/tempo'
74
68
  *
75
69
  * const result = TempoAddress.parse(
76
- * 'tempo1wst8d6qejxtdg4y5r3zarvary0c5xw7kvmgh8pm',
70
+ * 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0',
77
71
  * )
78
- * // { address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28' }
72
+ * // @log: { address: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28', zoneId: undefined }
79
73
  * ```
80
74
  *
81
75
  * @example
@@ -84,66 +78,54 @@ export declare namespace format {
84
78
  * import { TempoAddress } from 'ox/tempo'
85
79
  *
86
80
  * const result = TempoAddress.parse(
87
- * 'tempoz1qwst8d6qejxtdg4y5r3zarvary0c5xw7kvmgh8pm',
81
+ * 'tempoz1qqqhgtf4e3nrfszn9yj68wzyhj08t90jh55q74d9uj',
88
82
  * )
89
- * // { address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28', zoneId: 1 }
83
+ * // @log: { address: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28', zoneId: 1 }
90
84
  * ```
91
85
  *
92
86
  * @param tempoAddress - The Tempo address string to parse.
93
87
  * @returns The parsed raw address and optional zone ID.
94
88
  */
95
89
  export function parse(tempoAddress: string): parse.ReturnType {
96
- const lower = tempoAddress.toLowerCase()
97
-
98
- let prefix: string
99
- let hasZone: boolean
100
- if (lower.startsWith('tempoz1')) {
101
- prefix = 'tempoz1'
102
- hasZone = true
103
- } else if (lower.startsWith('tempo1')) {
104
- prefix = 'tempo1'
105
- hasZone = false
106
- } else {
107
- throw new InvalidPrefixError({ address: tempoAddress })
90
+ let hrp: string
91
+ let data: Uint8Array
92
+ try {
93
+ const decoded = Bech32m.decode(tempoAddress)
94
+ hrp = decoded.hrp
95
+ data = decoded.data
96
+ } catch {
97
+ throw new InvalidChecksumError({ address: tempoAddress })
108
98
  }
109
99
 
110
- const payload = Base32.toBytes(lower.slice(prefix.length))
100
+ if (hrp !== 'tempo' && hrp !== 'tempoz')
101
+ throw new InvalidPrefixError({ address: tempoAddress })
102
+
103
+ if (data.length < 1 || data[0] !== 0x00)
104
+ throw new InvalidVersionError({
105
+ address: tempoAddress,
106
+ version: data.length > 0 ? data[0]! : undefined,
107
+ })
108
+
109
+ const payload = data.slice(1)
111
110
 
112
- let zoneId: bigint | undefined
113
- let remaining: Uint8Array
114
- if (hasZone) {
111
+ let zoneId: number | bigint | undefined
112
+ let rawAddress: Uint8Array
113
+ if (hrp === 'tempoz') {
115
114
  const { value, size } = CompactSize.fromBytes(payload)
116
115
  zoneId = value
117
- remaining = payload.slice(size)
116
+ rawAddress = payload.slice(size)
118
117
  } else {
119
118
  zoneId = undefined
120
- remaining = payload
119
+ rawAddress = payload
121
120
  }
122
121
 
123
- if (remaining.length !== 24)
122
+ if (rawAddress.length !== 20)
124
123
  throw new InvalidLengthError({
125
124
  address: tempoAddress,
126
- expected: 24,
127
- actual: remaining.length,
125
+ expected: 20,
126
+ actual: rawAddress.length,
128
127
  })
129
128
 
130
- const rawAddress = remaining.slice(0, 20)
131
- const checksum = remaining.slice(20, 24)
132
-
133
- const zoneBytes =
134
- zoneId != null ? CompactSize.toBytes(zoneId) : new Uint8Array()
135
- const checksumInput = Bytes.concat(
136
- Bytes.fromString(prefix),
137
- zoneBytes,
138
- rawAddress,
139
- )
140
- const expected = Hash.sha256(Hash.sha256(checksumInput, { as: 'Bytes' }), {
141
- as: 'Bytes',
142
- }).slice(0, 4)
143
-
144
- if (!Bytes.isEqual(checksum, expected))
145
- throw new InvalidChecksumError({ address: tempoAddress })
146
-
147
129
  const address = Address.checksum(Hex.fromBytes(rawAddress) as Address.Address)
148
130
 
149
131
  return { address, zoneId }
@@ -154,11 +136,12 @@ export declare namespace parse {
154
136
  /** The raw 20-byte Ethereum address. */
155
137
  address: Address.Address
156
138
  /** The zone ID, or `undefined` for mainnet addresses. */
157
- zoneId: bigint | undefined
139
+ zoneId: number | bigint | undefined
158
140
  }
159
141
 
160
142
  type ErrorType =
161
143
  | InvalidPrefixError
144
+ | InvalidVersionError
162
145
  | InvalidLengthError
163
146
  | InvalidChecksumError
164
147
  | Errors.GlobalErrorType
@@ -172,9 +155,9 @@ export declare namespace parse {
172
155
  * import { TempoAddress } from 'ox/tempo'
173
156
  *
174
157
  * const valid = TempoAddress.validate(
175
- * 'tempo1wst8d6qejxtdg4y5r3zarvary0c5xw7kvmgh8pm',
158
+ * 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0',
176
159
  * )
177
- * // true
160
+ * // @log: true
178
161
  * ```
179
162
  *
180
163
  * @param tempoAddress - The Tempo address string to validate.
@@ -200,6 +183,20 @@ export class InvalidPrefixError extends Errors.BaseError {
200
183
  }
201
184
  }
202
185
 
186
+ /** Thrown when a Tempo address has an unsupported version byte. */
187
+ export class InvalidVersionError extends Errors.BaseError {
188
+ override readonly name = 'TempoAddress.InvalidVersionError'
189
+
190
+ constructor({
191
+ address,
192
+ version,
193
+ }: { address: string; version: number | undefined }) {
194
+ super(
195
+ `Tempo address "${address}" has unsupported version ${version === undefined ? '(missing)' : `0x${version.toString(16).padStart(2, '0')}`}. Only version 0x00 is supported.`,
196
+ )
197
+ }
198
+ }
199
+
203
200
  /** Thrown when a Tempo address has an invalid payload length. */
204
201
  export class InvalidLengthError extends Errors.BaseError {
205
202
  override readonly name = 'TempoAddress.InvalidLengthError'
@@ -164,6 +164,7 @@ describe('fromRpc', () => {
164
164
  },
165
165
  ],
166
166
  keyAuthorization: {
167
+ chainId: '0x1',
167
168
  expiry: '0xffffffffffff',
168
169
  keyId: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
169
170
  keyType: 'secp256k1',
@@ -224,7 +225,7 @@ describe('fromRpc', () => {
224
225
  "hash": "0x353fdfc38a2f26115daadee9f5b8392ce62b84f410957967e2ed56b35338cdd0",
225
226
  "keyAuthorization": {
226
227
  "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
227
- "chainId": 0n,
228
+ "chainId": 1n,
228
229
  "expiry": 281474976710655,
229
230
  "limits": [
230
231
  {
@@ -431,6 +432,7 @@ describe('toRpc', () => {
431
432
  data: undefined,
432
433
  keyAuthorization: {
433
434
  address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
435
+ chainId: 1n,
434
436
  expiry: 281474976710655,
435
437
  type: 'secp256k1',
436
438
  limits: [
@@ -487,7 +489,7 @@ describe('toRpc', () => {
487
489
  "hash": "0x353fdfc38a2f26115daadee9f5b8392ce62b84f410957967e2ed56b35338cdd0",
488
490
  "input": undefined,
489
491
  "keyAuthorization": {
490
- "chainId": "0x",
492
+ "chainId": "0x1",
491
493
  "expiry": "0xffffffffffff",
492
494
  "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
493
495
  "keyType": "secp256k1",