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.
- package/Base32/package.json +6 -0
- package/Bech32m/package.json +6 -0
- package/CHANGELOG.md +25 -0
- package/CompactSize/package.json +6 -0
- package/_cjs/core/Base32.js +73 -0
- package/_cjs/core/Base32.js.map +1 -0
- package/_cjs/core/Bech32m.js +205 -0
- package/_cjs/core/Bech32m.js.map +1 -0
- package/_cjs/core/CompactSize.js +91 -0
- package/_cjs/core/CompactSize.js.map +1 -0
- package/_cjs/index.js +5 -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 +117 -0
- package/_cjs/tempo/TempoAddress.js.map +1 -0
- package/_cjs/tempo/TxEnvelopeTempo.js +5 -2
- package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
- package/_cjs/tempo/index.js +2 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/core/Base32.js +119 -0
- package/_esm/core/Base32.js.map +1 -0
- package/_esm/core/Bech32m.js +238 -0
- package/_esm/core/Bech32m.js.map +1 -0
- package/_esm/core/CompactSize.js +150 -0
- package/_esm/core/CompactSize.js.map +1 -0
- package/_esm/index.js +72 -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 +182 -0
- package/_esm/tempo/TempoAddress.js.map +1 -0
- package/_esm/tempo/TxEnvelopeTempo.js +42 -2
- package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
- package/_esm/tempo/index.js +21 -0
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/core/Base32.d.ts +79 -0
- package/_types/core/Base32.d.ts.map +1 -0
- package/_types/core/Bech32m.d.ts +93 -0
- package/_types/core/Bech32m.d.ts.map +1 -0
- package/_types/core/CompactSize.d.ts +107 -0
- package/_types/core/CompactSize.d.ts.map +1 -0
- package/_types/index.d.ts +72 -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 +126 -0
- package/_types/tempo/TempoAddress.d.ts.map +1 -0
- package/_types/tempo/TxEnvelopeTempo.d.ts +47 -1
- package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +21 -0
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/core/Base32.ts +134 -0
- package/core/Bech32m.ts +263 -0
- package/core/CompactSize.ts +169 -0
- package/index.ts +74 -1
- package/package.json +21 -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/package.json +6 -0
- package/tempo/TempoAddress.test.ts +237 -0
- package/tempo/TempoAddress.ts +222 -0
- 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 +22 -0
- 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
|
|
38
|
-
chainId
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
|
@@ -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
|
+
})
|