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.
Files changed (68) hide show
  1. package/Base32/package.json +6 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CompactSize/package.json +6 -0
  4. package/_cjs/core/Base32.js +73 -0
  5. package/_cjs/core/Base32.js.map +1 -0
  6. package/_cjs/core/CompactSize.js +91 -0
  7. package/_cjs/core/CompactSize.js.map +1 -0
  8. package/_cjs/index.js +4 -2
  9. package/_cjs/index.js.map +1 -1
  10. package/_cjs/tempo/KeyAuthorization.js +18 -3
  11. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  12. package/_cjs/tempo/SignatureEnvelope.js +26 -0
  13. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  14. package/_cjs/tempo/TempoAddress.js +116 -0
  15. package/_cjs/tempo/TempoAddress.js.map +1 -0
  16. package/_cjs/tempo/TxEnvelopeTempo.js +5 -10
  17. package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
  18. package/_cjs/tempo/index.js +2 -1
  19. package/_cjs/tempo/index.js.map +1 -1
  20. package/_cjs/version.js +1 -1
  21. package/_esm/core/Base32.js +119 -0
  22. package/_esm/core/Base32.js.map +1 -0
  23. package/_esm/core/CompactSize.js +150 -0
  24. package/_esm/core/CompactSize.js.map +1 -0
  25. package/_esm/index.js +48 -0
  26. package/_esm/index.js.map +1 -1
  27. package/_esm/tempo/KeyAuthorization.js +66 -3
  28. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  29. package/_esm/tempo/SignatureEnvelope.js +74 -0
  30. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  31. package/_esm/tempo/TempoAddress.js +181 -0
  32. package/_esm/tempo/TempoAddress.js.map +1 -0
  33. package/_esm/tempo/TxEnvelopeTempo.js +5 -10
  34. package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
  35. package/_esm/tempo/index.js +21 -0
  36. package/_esm/tempo/index.js.map +1 -1
  37. package/_esm/version.js +1 -1
  38. package/_types/core/Base32.d.ts +79 -0
  39. package/_types/core/Base32.d.ts.map +1 -0
  40. package/_types/core/CompactSize.d.ts +107 -0
  41. package/_types/core/CompactSize.d.ts.map +1 -0
  42. package/_types/index.d.ts +48 -0
  43. package/_types/index.d.ts.map +1 -1
  44. package/_types/tempo/KeyAuthorization.d.ts +57 -0
  45. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  46. package/_types/tempo/SignatureEnvelope.d.ts +75 -0
  47. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  48. package/_types/tempo/TempoAddress.d.ts +118 -0
  49. package/_types/tempo/TempoAddress.d.ts.map +1 -0
  50. package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  51. package/_types/tempo/index.d.ts +21 -0
  52. package/_types/tempo/index.d.ts.map +1 -1
  53. package/_types/version.d.ts +1 -1
  54. package/core/Base32.ts +134 -0
  55. package/core/CompactSize.ts +169 -0
  56. package/index.ts +51 -0
  57. package/package.json +16 -1
  58. package/tempo/KeyAuthorization.test.ts +139 -0
  59. package/tempo/KeyAuthorization.ts +82 -3
  60. package/tempo/SignatureEnvelope.test.ts +147 -0
  61. package/tempo/SignatureEnvelope.ts +113 -0
  62. package/tempo/TempoAddress/package.json +6 -0
  63. package/tempo/TempoAddress.test.ts +202 -0
  64. package/tempo/TempoAddress.ts +225 -0
  65. package/tempo/TxEnvelopeTempo.ts +5 -12
  66. package/tempo/e2e.test.ts +265 -0
  67. package/tempo/index.ts +21 -3
  68. 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,6 @@
1
+ {
2
+ "type": "module",
3
+ "types": "../../_types/tempo/TempoAddress.d.ts",
4
+ "main": "../../_cjs/tempo/TempoAddress.js",
5
+ "module": "../../_esm/tempo/TempoAddress.js"
6
+ }
@@ -0,0 +1,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
+ }
@@ -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
- if (signatureEnvelope.type === 'keychain')
382
- transaction.from = signatureEnvelope.userAddress
383
- else if (
384
- signatureEnvelope.type === 'p256' ||
385
- signatureEnvelope.type === 'webAuthn'
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