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.
- package/Bech32m/package.json +6 -0
- package/CHANGELOG.md +19 -0
- package/_cjs/core/Bech32m.js +205 -0
- package/_cjs/core/Bech32m.js.map +1 -0
- package/_cjs/index.js +3 -2
- package/_cjs/index.js.map +1 -1
- package/_cjs/tempo/KeyAuthorization.js +4 -4
- package/_cjs/tempo/KeyAuthorization.js.map +1 -1
- package/_cjs/tempo/SignatureEnvelope.js +18 -3
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/TempoAddress.js +40 -39
- package/_cjs/tempo/TempoAddress.js.map +1 -1
- package/_cjs/tempo/TxEnvelopeTempo.js +5 -2
- package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/core/Bech32m.js +238 -0
- package/_esm/core/Bech32m.js.map +1 -0
- package/_esm/index.js +24 -0
- package/_esm/index.js.map +1 -1
- package/_esm/tempo/KeyAuthorization.js +19 -9
- package/_esm/tempo/KeyAuthorization.js.map +1 -1
- package/_esm/tempo/SignatureEnvelope.js +22 -5
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/TempoAddress.js +47 -46
- package/_esm/tempo/TempoAddress.js.map +1 -1
- package/_esm/tempo/TxEnvelopeTempo.js +42 -2
- package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
- package/_esm/tempo/index.js +2 -2
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/core/Bech32m.d.ts +93 -0
- package/_types/core/Bech32m.d.ts.map +1 -0
- package/_types/index.d.ts +24 -0
- package/_types/index.d.ts.map +1 -1
- package/_types/tempo/KeyAuthorization.d.ts +17 -7
- package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
- package/_types/tempo/SignatureEnvelope.d.ts +19 -5
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/TempoAddress.d.ts +19 -11
- package/_types/tempo/TempoAddress.d.ts.map +1 -1
- package/_types/tempo/TxEnvelopeTempo.d.ts +47 -1
- package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +2 -2
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/core/Bech32m.ts +263 -0
- package/index.ts +24 -2
- package/package.json +6 -1
- package/tempo/KeyAuthorization.test.ts +70 -23
- package/tempo/KeyAuthorization.ts +21 -18
- package/tempo/SignatureEnvelope.test.ts +15 -8
- package/tempo/SignatureEnvelope.ts +43 -8
- package/tempo/TempoAddress.test.ts +49 -14
- package/tempo/TempoAddress.ts +56 -59
- package/tempo/Transaction.test.ts +4 -2
- package/tempo/TxEnvelopeTempo.test.ts +7 -3
- package/tempo/TxEnvelopeTempo.ts +52 -1
- package/tempo/e2e.test.ts +45 -10
- package/tempo/index.ts +6 -2
- 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)
|
|
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)
|
|
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 (
|
|
1254
|
-
expect(Hex.slice(serialized, 0, 1)).toBe('
|
|
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 (
|
|
1269
|
-
expect(Hex.slice(serialized, 0, 1)).toBe('
|
|
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 (
|
|
1288
|
-
expect(Hex.slice(serialized, 0, 1)).toBe('
|
|
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
|
|
90
|
-
* p256, or webAuthn). Format:
|
|
91
|
-
*
|
|
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 (
|
|
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)
|
|
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
|
-
|
|
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
|
-
`"
|
|
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(
|
|
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(
|
|
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
|
-
`"
|
|
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
|
-
`"
|
|
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
|
-
`"
|
|
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
|
-
`"
|
|
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
|
-
`"
|
|
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('
|
|
173
|
+
test('all uppercase', () => {
|
|
148
174
|
const encoded = TempoAddress.format(rawAddress)
|
|
149
|
-
const upper = encoded.
|
|
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(
|
|
182
|
+
TempoAddress.parse(encoded),
|
|
156
183
|
).toThrowErrorMatchingInlineSnapshot(
|
|
157
|
-
`[TempoAddress.InvalidPrefixError: Tempo address "
|
|
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.
|
|
212
|
+
`[TempoAddress.InvalidChecksumError: Tempo address "tempoz1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0" has an invalid checksum.]`,
|
|
178
213
|
)
|
|
179
214
|
})
|
|
180
215
|
})
|
package/tempo/TempoAddress.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import * as Address from '../core/Address.js'
|
|
2
|
-
import * as
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
47
|
+
const data = Bytes.concat(version, zone, Bytes.fromHex(address))
|
|
48
48
|
|
|
49
|
-
|
|
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?:
|
|
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
|
-
* '
|
|
70
|
+
* 'tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0',
|
|
77
71
|
* )
|
|
78
|
-
* // { address: '
|
|
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
|
-
* '
|
|
81
|
+
* 'tempoz1qqqhgtf4e3nrfszn9yj68wzyhj08t90jh55q74d9uj',
|
|
88
82
|
* )
|
|
89
|
-
* // { address: '
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
114
|
-
if (
|
|
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
|
-
|
|
116
|
+
rawAddress = payload.slice(size)
|
|
118
117
|
} else {
|
|
119
118
|
zoneId = undefined
|
|
120
|
-
|
|
119
|
+
rawAddress = payload
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
if (
|
|
122
|
+
if (rawAddress.length !== 20)
|
|
124
123
|
throw new InvalidLengthError({
|
|
125
124
|
address: tempoAddress,
|
|
126
|
-
expected:
|
|
127
|
-
actual:
|
|
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
|
-
* '
|
|
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":
|
|
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": "
|
|
492
|
+
"chainId": "0x1",
|
|
491
493
|
"expiry": "0xffffffffffff",
|
|
492
494
|
"keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c",
|
|
493
495
|
"keyType": "secp256k1",
|