ox 0.13.0 → 0.13.2
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/CHANGELOG.md +16 -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/CompactSize.js +91 -0
- package/_cjs/core/CompactSize.js.map +1 -0
- package/_cjs/index.js +4 -2
- package/_cjs/index.js.map +1 -1
- package/_cjs/tempo/KeyAuthorization.js +18 -3
- package/_cjs/tempo/KeyAuthorization.js.map +1 -1
- package/_cjs/tempo/SignatureEnvelope.js +26 -0
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/TempoAddress.js +116 -0
- package/_cjs/tempo/TempoAddress.js.map +1 -0
- package/_cjs/tempo/TxEnvelopeTempo.js +5 -10
- 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/CompactSize.js +150 -0
- package/_esm/core/CompactSize.js.map +1 -0
- package/_esm/index.js +48 -0
- package/_esm/index.js.map +1 -1
- package/_esm/tempo/KeyAuthorization.js +66 -3
- package/_esm/tempo/KeyAuthorization.js.map +1 -1
- package/_esm/tempo/SignatureEnvelope.js +74 -0
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/TempoAddress.js +181 -0
- package/_esm/tempo/TempoAddress.js.map +1 -0
- package/_esm/tempo/TxEnvelopeTempo.js +5 -10
- 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/CompactSize.d.ts +107 -0
- package/_types/core/CompactSize.d.ts.map +1 -0
- package/_types/index.d.ts +48 -0
- package/_types/index.d.ts.map +1 -1
- package/_types/tempo/KeyAuthorization.d.ts +57 -0
- package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
- package/_types/tempo/SignatureEnvelope.d.ts +75 -0
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/TempoAddress.d.ts +118 -0
- package/_types/tempo/TempoAddress.d.ts.map +1 -0
- 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/CompactSize.ts +169 -0
- package/index.ts +51 -0
- package/package.json +16 -1
- package/tempo/KeyAuthorization.test.ts +139 -0
- package/tempo/KeyAuthorization.ts +82 -3
- package/tempo/SignatureEnvelope.test.ts +147 -0
- package/tempo/SignatureEnvelope.ts +113 -0
- package/tempo/TempoAddress/package.json +6 -0
- package/tempo/TempoAddress.test.ts +202 -0
- package/tempo/TempoAddress.ts +225 -0
- package/tempo/TxEnvelopeTempo.ts +5 -12
- package/tempo/e2e.test.ts +265 -0
- package/tempo/index.ts +21 -3
- package/version.ts +1 -1
|
@@ -269,6 +269,119 @@ export declare namespace assert {
|
|
|
269
269
|
| Errors.GlobalErrorType
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Extracts the address of the signer from a {@link ox#SignatureEnvelope.SignatureEnvelope}.
|
|
274
|
+
*
|
|
275
|
+
* - **secp256k1**: Recovers the address from the payload via `ecrecover`.
|
|
276
|
+
* - **p256** / **webAuthn**: Derives the address from the embedded public key.
|
|
277
|
+
* - **keychain**: Extracts from the inner signature (or returns `userAddress` if `user` is `true`).
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```ts twoslash
|
|
281
|
+
* import { Secp256k1 } from 'ox'
|
|
282
|
+
* import { SignatureEnvelope } from 'ox/tempo'
|
|
283
|
+
*
|
|
284
|
+
* const payload = '0xdeadbeef'
|
|
285
|
+
* const signature = Secp256k1.sign({ payload, privateKey: '0x...' })
|
|
286
|
+
* const envelope = SignatureEnvelope.from(signature)
|
|
287
|
+
*
|
|
288
|
+
* const address = SignatureEnvelope.extractAddress({ // [!code focus]
|
|
289
|
+
* payload, // [!code focus]
|
|
290
|
+
* signature: envelope, // [!code focus]
|
|
291
|
+
* }) // [!code focus]
|
|
292
|
+
* ```
|
|
293
|
+
*
|
|
294
|
+
* @param options - The extraction options.
|
|
295
|
+
* @returns The signer address.
|
|
296
|
+
*/
|
|
297
|
+
export function extractAddress(
|
|
298
|
+
options: extractAddress.Options,
|
|
299
|
+
): extractAddress.ReturnType {
|
|
300
|
+
const { signature, root } = options
|
|
301
|
+
if (signature.type === 'keychain') {
|
|
302
|
+
if (root) return signature.userAddress
|
|
303
|
+
return extractAddress({ ...options, signature: signature.inner })
|
|
304
|
+
}
|
|
305
|
+
return Address.fromPublicKey(extractPublicKey(options))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export declare namespace extractAddress {
|
|
309
|
+
type Options = {
|
|
310
|
+
/** The sign payload that was signed (only required for secp256k1 signatures). */
|
|
311
|
+
payload: Hex.Hex | Bytes.Bytes
|
|
312
|
+
/** The signature envelope. */
|
|
313
|
+
signature: SignatureEnvelope
|
|
314
|
+
/** Whether to return the root `userAddress` for keychain signatures instead of extracting from the inner signature. */
|
|
315
|
+
root?: boolean | undefined
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
type ReturnType = Address.Address
|
|
319
|
+
|
|
320
|
+
type ErrorType =
|
|
321
|
+
| Address.fromPublicKey.ErrorType
|
|
322
|
+
| extractPublicKey.ErrorType
|
|
323
|
+
| Errors.GlobalErrorType
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extracts the public key of the signer from a {@link ox#SignatureEnvelope.SignatureEnvelope}.
|
|
328
|
+
*
|
|
329
|
+
* - **secp256k1**: Recovers the public key from the payload via `ecrecover`.
|
|
330
|
+
* - **p256** / **webAuthn**: Returns the embedded public key.
|
|
331
|
+
* - **keychain**: Extracts from the inner signature.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts twoslash
|
|
335
|
+
* import { Secp256k1 } from 'ox'
|
|
336
|
+
* import { SignatureEnvelope } from 'ox/tempo'
|
|
337
|
+
*
|
|
338
|
+
* const payload = '0xdeadbeef'
|
|
339
|
+
* const signature = Secp256k1.sign({ payload, privateKey: '0x...' })
|
|
340
|
+
* const envelope = SignatureEnvelope.from(signature)
|
|
341
|
+
*
|
|
342
|
+
* const publicKey = SignatureEnvelope.extractPublicKey({ // [!code focus]
|
|
343
|
+
* payload, // [!code focus]
|
|
344
|
+
* signature: envelope, // [!code focus]
|
|
345
|
+
* }) // [!code focus]
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* @param options - The extraction options.
|
|
349
|
+
* @returns The signer's public key.
|
|
350
|
+
*/
|
|
351
|
+
export function extractPublicKey(
|
|
352
|
+
options: extractPublicKey.Options,
|
|
353
|
+
): extractPublicKey.ReturnType {
|
|
354
|
+
const { payload, signature } = options
|
|
355
|
+
|
|
356
|
+
switch (signature.type) {
|
|
357
|
+
case 'secp256k1':
|
|
358
|
+
return ox_Secp256k1.recoverPublicKey({
|
|
359
|
+
payload,
|
|
360
|
+
signature: signature.signature,
|
|
361
|
+
})
|
|
362
|
+
case 'p256':
|
|
363
|
+
case 'webAuthn':
|
|
364
|
+
return signature.publicKey
|
|
365
|
+
case 'keychain':
|
|
366
|
+
return extractPublicKey({ payload, signature: signature.inner })
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export declare namespace extractPublicKey {
|
|
371
|
+
type Options = {
|
|
372
|
+
/** The sign payload that was signed (only required for secp256k1 signatures). */
|
|
373
|
+
payload: Hex.Hex | Bytes.Bytes
|
|
374
|
+
/** The signature envelope. */
|
|
375
|
+
signature: SignatureEnvelope
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
type ReturnType = PublicKey.PublicKey
|
|
379
|
+
|
|
380
|
+
type ErrorType =
|
|
381
|
+
| ox_Secp256k1.recoverPublicKey.ErrorType
|
|
382
|
+
| Errors.GlobalErrorType
|
|
383
|
+
}
|
|
384
|
+
|
|
272
385
|
/**
|
|
273
386
|
* Deserializes a hex-encoded signature envelope into a typed signature object.
|
|
274
387
|
*
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Address } 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
|
+
`"tempo1wskntnrxxnq9x2f95wuyf0y7wk2l90fg8zd8djs"`,
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('zone address (zone ID = 1)', () => {
|
|
17
|
+
expect(
|
|
18
|
+
TempoAddress.format(rawAddress, { zoneId: 1 }),
|
|
19
|
+
).toMatchInlineSnapshot(`"tempoz1q96z6dwvvc6vq5efyk3ms39une6etu4a9zeqtx3q"`)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('zone address (zone ID = 252)', () => {
|
|
23
|
+
expect(
|
|
24
|
+
TempoAddress.format(rawAddress, { zoneId: 252 }),
|
|
25
|
+
).toMatchInlineSnapshot(`"tempoz1l36z6dwvvc6vq5efyk3ms39une6etu4a9z8vgw44"`)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('zone address (zone ID = 253)', () => {
|
|
29
|
+
expect(
|
|
30
|
+
TempoAddress.format(rawAddress, { zoneId: 253 }),
|
|
31
|
+
).toMatchInlineSnapshot(
|
|
32
|
+
`"tempoz1lh7sqapdxhxxvdxq2v5jtgacgj7fuav4727jsx0032us"`,
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('zone address (zone ID = 65535)', () => {
|
|
37
|
+
expect(
|
|
38
|
+
TempoAddress.format(rawAddress, { zoneId: 65535 }),
|
|
39
|
+
).toMatchInlineSnapshot(
|
|
40
|
+
`"tempoz1lhll7apdxhxxvdxq2v5jtgacgj7fuav4727j37cldu7q"`,
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('zone address (zone ID = 65536)', () => {
|
|
45
|
+
expect(
|
|
46
|
+
TempoAddress.format(rawAddress, { zoneId: 65536 }),
|
|
47
|
+
).toMatchInlineSnapshot(
|
|
48
|
+
`"tempoz1lcqqqqgqwskntnrxxnq9x2f95wuyf0y7wk2l90fga965qjc"`,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('zone address (zone ID = 4294967295)', () => {
|
|
53
|
+
expect(
|
|
54
|
+
TempoAddress.format(rawAddress, { zoneId: 4294967295 }),
|
|
55
|
+
).toMatchInlineSnapshot(
|
|
56
|
+
`"tempoz1lmllllllwskntnrxxnq9x2f95wuyf0y7wk2l90fgxg58ulq"`,
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('zone address (zone ID > 4294967295)', () => {
|
|
61
|
+
expect(
|
|
62
|
+
TempoAddress.format(rawAddress, { zoneId: BigInt('4294967296') }),
|
|
63
|
+
).toMatchInlineSnapshot(
|
|
64
|
+
`"tempoz1luqqqqqqqyqqqqr5956uce35cpfjjfdrhpzte8n4jhet62pnyj7cc"`,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('lowercase output', () => {
|
|
69
|
+
const result = TempoAddress.format(rawAddress)
|
|
70
|
+
expect(result).toBe(result.toLowerCase())
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('parse', () => {
|
|
75
|
+
test('mainnet', () => {
|
|
76
|
+
const encoded = TempoAddress.format(rawAddress)
|
|
77
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
78
|
+
{
|
|
79
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
80
|
+
"zoneId": undefined,
|
|
81
|
+
}
|
|
82
|
+
`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('zone (zone ID = 1)', () => {
|
|
86
|
+
const encoded = TempoAddress.format(rawAddress, { zoneId: 1 })
|
|
87
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
88
|
+
{
|
|
89
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
90
|
+
"zoneId": 1n,
|
|
91
|
+
}
|
|
92
|
+
`)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('zone (zone ID = 252)', () => {
|
|
96
|
+
const encoded = TempoAddress.format(rawAddress, { zoneId: 252 })
|
|
97
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
98
|
+
{
|
|
99
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
100
|
+
"zoneId": 252n,
|
|
101
|
+
}
|
|
102
|
+
`)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('zone (zone ID = 253)', () => {
|
|
106
|
+
const encoded = TempoAddress.format(rawAddress, { zoneId: 253 })
|
|
107
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
108
|
+
{
|
|
109
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
110
|
+
"zoneId": 253n,
|
|
111
|
+
}
|
|
112
|
+
`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('zone (zone ID = 65535)', () => {
|
|
116
|
+
const encoded = TempoAddress.format(rawAddress, { zoneId: 65535 })
|
|
117
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
118
|
+
{
|
|
119
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
120
|
+
"zoneId": 65535n,
|
|
121
|
+
}
|
|
122
|
+
`)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('zone (zone ID = 65536)', () => {
|
|
126
|
+
const encoded = TempoAddress.format(rawAddress, { zoneId: 65536 })
|
|
127
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
128
|
+
{
|
|
129
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
130
|
+
"zoneId": 65536n,
|
|
131
|
+
}
|
|
132
|
+
`)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('zone (large zone ID)', () => {
|
|
136
|
+
const encoded = TempoAddress.format(rawAddress, {
|
|
137
|
+
zoneId: BigInt('18446744073709551615'),
|
|
138
|
+
})
|
|
139
|
+
expect(TempoAddress.parse(encoded)).toMatchInlineSnapshot(`
|
|
140
|
+
{
|
|
141
|
+
"address": "0x742d35CC6634c0532925a3B844bc9e7595F2Bd28",
|
|
142
|
+
"zoneId": 18446744073709551615n,
|
|
143
|
+
}
|
|
144
|
+
`)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('case insensitive', () => {
|
|
148
|
+
const encoded = TempoAddress.format(rawAddress)
|
|
149
|
+
const upper = encoded.slice(0, 6) + encoded.slice(6).toUpperCase()
|
|
150
|
+
expect(TempoAddress.parse(upper).address).toBe(rawAddress)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('error: invalid prefix', () => {
|
|
154
|
+
expect(() =>
|
|
155
|
+
TempoAddress.parse('bitcoin1abc'),
|
|
156
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
157
|
+
`[TempoAddress.InvalidPrefixError: Tempo address "bitcoin1abc" has an invalid prefix. Expected "tempo1" or "tempoz1".]`,
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('error: invalid checksum', () => {
|
|
162
|
+
const encoded = TempoAddress.format(rawAddress)
|
|
163
|
+
const tampered = encoded.slice(0, -1) + (encoded.endsWith('q') ? 'p' : 'q')
|
|
164
|
+
expect(() =>
|
|
165
|
+
TempoAddress.parse(tampered),
|
|
166
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
167
|
+
`[TempoAddress.InvalidChecksumError: Tempo address "${tampered}" has an invalid checksum.]`,
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('error: prefix swap detected', () => {
|
|
172
|
+
const mainnet = TempoAddress.format(rawAddress)
|
|
173
|
+
const swapped = 'tempoz1' + mainnet.slice(6)
|
|
174
|
+
expect(() =>
|
|
175
|
+
TempoAddress.parse(swapped),
|
|
176
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
177
|
+
`[TempoAddress.InvalidLengthError: Tempo address "${swapped}" has an invalid payload length. Expected 24 bytes, got 23.]`,
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('validate', () => {
|
|
183
|
+
test('valid mainnet address', () => {
|
|
184
|
+
const encoded = TempoAddress.format(rawAddress)
|
|
185
|
+
expect(TempoAddress.validate(encoded)).toMatchInlineSnapshot(`true`)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('valid zone address', () => {
|
|
189
|
+
const encoded = TempoAddress.format(rawAddress, { zoneId: 1 })
|
|
190
|
+
expect(TempoAddress.validate(encoded)).toMatchInlineSnapshot(`true`)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('invalid address', () => {
|
|
194
|
+
expect(TempoAddress.validate('invalid')).toMatchInlineSnapshot(`false`)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('tampered address', () => {
|
|
198
|
+
const encoded = TempoAddress.format(rawAddress)
|
|
199
|
+
const tampered = encoded.slice(0, -1) + (encoded.endsWith('q') ? 'p' : 'q')
|
|
200
|
+
expect(TempoAddress.validate(tampered)).toMatchInlineSnapshot(`false`)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as Address from '../core/Address.js'
|
|
2
|
+
import * as Base32 from '../core/Base32.js'
|
|
3
|
+
import * as Bytes from '../core/Bytes.js'
|
|
4
|
+
import * as CompactSize from '../core/CompactSize.js'
|
|
5
|
+
import * as Errors from '../core/Errors.js'
|
|
6
|
+
import * as Hash from '../core/Hash.js'
|
|
7
|
+
import * as Hex from '../core/Hex.js'
|
|
8
|
+
|
|
9
|
+
/** Root type for a Tempo Address. */
|
|
10
|
+
export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Formats a raw Ethereum address (and optional zone ID) into a Tempo address string.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts twoslash
|
|
17
|
+
* import { TempoAddress } from 'ox/tempo'
|
|
18
|
+
*
|
|
19
|
+
* const address = TempoAddress.format('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28')
|
|
20
|
+
* // @log: 'tempo1wskntnrxxnq9x2f95wuyf0y7wk2l90fg8zd8djs'
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ### Zone Address
|
|
25
|
+
* ```ts twoslash
|
|
26
|
+
* import { TempoAddress } from 'ox/tempo'
|
|
27
|
+
*
|
|
28
|
+
* const address = TempoAddress.format(
|
|
29
|
+
* '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28',
|
|
30
|
+
* { zoneId: 1 },
|
|
31
|
+
* )
|
|
32
|
+
* // @log: 'tempoz1q96z6dwvvc6vq5efyk3ms39une6etu4a9zeqtx3q'
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @param address - The raw 20-byte Ethereum address.
|
|
36
|
+
* @param options - Options.
|
|
37
|
+
* @returns The encoded Tempo address string.
|
|
38
|
+
*/
|
|
39
|
+
export function format(
|
|
40
|
+
address: Address.Address,
|
|
41
|
+
options: format.Options = {},
|
|
42
|
+
): TempoAddress {
|
|
43
|
+
const { zoneId } = options
|
|
44
|
+
|
|
45
|
+
const prefix = zoneId != null ? 'tempoz1' : 'tempo1'
|
|
46
|
+
const zone = zoneId != null ? CompactSize.toBytes(zoneId) : new Uint8Array()
|
|
47
|
+
const address_bytes = Bytes.fromHex(address)
|
|
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
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export declare namespace format {
|
|
59
|
+
type Options = {
|
|
60
|
+
/** Zone ID for zone addresses. */
|
|
61
|
+
zoneId?: bigint | number | undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ErrorType = Errors.GlobalErrorType
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parses a Tempo address string into a raw Ethereum address and optional zone ID.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ### Mainnet Address
|
|
72
|
+
* ```ts twoslash
|
|
73
|
+
* import { TempoAddress } from 'ox/tempo'
|
|
74
|
+
*
|
|
75
|
+
* const result = TempoAddress.parse(
|
|
76
|
+
* 'tempo1wst8d6qejxtdg4y5r3zarvary0c5xw7kvmgh8pm',
|
|
77
|
+
* )
|
|
78
|
+
* // { address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28' }
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ### Zone Address
|
|
83
|
+
* ```ts twoslash
|
|
84
|
+
* import { TempoAddress } from 'ox/tempo'
|
|
85
|
+
*
|
|
86
|
+
* const result = TempoAddress.parse(
|
|
87
|
+
* 'tempoz1qwst8d6qejxtdg4y5r3zarvary0c5xw7kvmgh8pm',
|
|
88
|
+
* )
|
|
89
|
+
* // { address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28', zoneId: 1 }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @param tempoAddress - The Tempo address string to parse.
|
|
93
|
+
* @returns The parsed raw address and optional zone ID.
|
|
94
|
+
*/
|
|
95
|
+
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 })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const payload = Base32.toBytes(lower.slice(prefix.length))
|
|
111
|
+
|
|
112
|
+
let zoneId: bigint | undefined
|
|
113
|
+
let remaining: Uint8Array
|
|
114
|
+
if (hasZone) {
|
|
115
|
+
const { value, size } = CompactSize.fromBytes(payload)
|
|
116
|
+
zoneId = value
|
|
117
|
+
remaining = payload.slice(size)
|
|
118
|
+
} else {
|
|
119
|
+
zoneId = undefined
|
|
120
|
+
remaining = payload
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (remaining.length !== 24)
|
|
124
|
+
throw new InvalidLengthError({
|
|
125
|
+
address: tempoAddress,
|
|
126
|
+
expected: 24,
|
|
127
|
+
actual: remaining.length,
|
|
128
|
+
})
|
|
129
|
+
|
|
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
|
+
const address = Address.checksum(Hex.fromBytes(rawAddress) as Address.Address)
|
|
148
|
+
|
|
149
|
+
return { address, zoneId }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export declare namespace parse {
|
|
153
|
+
type ReturnType = {
|
|
154
|
+
/** The raw 20-byte Ethereum address. */
|
|
155
|
+
address: Address.Address
|
|
156
|
+
/** The zone ID, or `undefined` for mainnet addresses. */
|
|
157
|
+
zoneId: bigint | undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type ErrorType =
|
|
161
|
+
| InvalidPrefixError
|
|
162
|
+
| InvalidLengthError
|
|
163
|
+
| InvalidChecksumError
|
|
164
|
+
| Errors.GlobalErrorType
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validates a Tempo address string.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts twoslash
|
|
172
|
+
* import { TempoAddress } from 'ox/tempo'
|
|
173
|
+
*
|
|
174
|
+
* const valid = TempoAddress.validate(
|
|
175
|
+
* 'tempo1wst8d6qejxtdg4y5r3zarvary0c5xw7kvmgh8pm',
|
|
176
|
+
* )
|
|
177
|
+
* // true
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* @param tempoAddress - The Tempo address string to validate.
|
|
181
|
+
* @returns Whether the address is valid.
|
|
182
|
+
*/
|
|
183
|
+
export function validate(tempoAddress: string): boolean {
|
|
184
|
+
try {
|
|
185
|
+
parse(tempoAddress)
|
|
186
|
+
return true
|
|
187
|
+
} catch {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Thrown when a Tempo address has an invalid prefix. */
|
|
193
|
+
export class InvalidPrefixError extends Errors.BaseError {
|
|
194
|
+
override readonly name = 'TempoAddress.InvalidPrefixError'
|
|
195
|
+
|
|
196
|
+
constructor({ address }: { address: string }) {
|
|
197
|
+
super(
|
|
198
|
+
`Tempo address "${address}" has an invalid prefix. Expected "tempo1" or "tempoz1".`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Thrown when a Tempo address has an invalid payload length. */
|
|
204
|
+
export class InvalidLengthError extends Errors.BaseError {
|
|
205
|
+
override readonly name = 'TempoAddress.InvalidLengthError'
|
|
206
|
+
|
|
207
|
+
constructor({
|
|
208
|
+
address,
|
|
209
|
+
expected,
|
|
210
|
+
actual,
|
|
211
|
+
}: { address: string; expected: number; actual: number }) {
|
|
212
|
+
super(
|
|
213
|
+
`Tempo address "${address}" has an invalid payload length. Expected ${expected} bytes, got ${actual}.`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Thrown when a Tempo address has an invalid checksum. */
|
|
219
|
+
export class InvalidChecksumError extends Errors.BaseError {
|
|
220
|
+
override readonly name = 'TempoAddress.InvalidChecksumError'
|
|
221
|
+
|
|
222
|
+
constructor({ address }: { address: string }) {
|
|
223
|
+
super(`Tempo address "${address}" has an invalid checksum.`)
|
|
224
|
+
}
|
|
225
|
+
}
|
package/tempo/TxEnvelopeTempo.ts
CHANGED
|
@@ -378,18 +378,11 @@ export function deserialize(serialized: Serialized): Compute<TxEnvelopeTempo> {
|
|
|
378
378
|
// Recover sender address from the signature if not already set.
|
|
379
379
|
if (!transaction.from && signatureEnvelope) {
|
|
380
380
|
try {
|
|
381
|
-
|
|
382
|
-
transaction
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
transaction.from = Address.fromPublicKey(signatureEnvelope.publicKey)
|
|
388
|
-
else if (signatureEnvelope.type === 'secp256k1')
|
|
389
|
-
transaction.from = Secp256k1.recoverAddress({
|
|
390
|
-
payload: getSignPayload(from(transaction)),
|
|
391
|
-
signature: signatureEnvelope.signature,
|
|
392
|
-
})
|
|
381
|
+
transaction.from = SignatureEnvelope.extractAddress({
|
|
382
|
+
payload: getSignPayload(from(transaction)),
|
|
383
|
+
signature: signatureEnvelope,
|
|
384
|
+
root: true,
|
|
385
|
+
})
|
|
393
386
|
} catch {}
|
|
394
387
|
}
|
|
395
388
|
|