ox 0.13.1 → 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 (45) hide show
  1. package/Base32/package.json +6 -0
  2. package/CHANGELOG.md +6 -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/TempoAddress.js +116 -0
  11. package/_cjs/tempo/TempoAddress.js.map +1 -0
  12. package/_cjs/tempo/index.js +2 -1
  13. package/_cjs/tempo/index.js.map +1 -1
  14. package/_cjs/version.js +1 -1
  15. package/_esm/core/Base32.js +119 -0
  16. package/_esm/core/Base32.js.map +1 -0
  17. package/_esm/core/CompactSize.js +150 -0
  18. package/_esm/core/CompactSize.js.map +1 -0
  19. package/_esm/index.js +48 -0
  20. package/_esm/index.js.map +1 -1
  21. package/_esm/tempo/TempoAddress.js +181 -0
  22. package/_esm/tempo/TempoAddress.js.map +1 -0
  23. package/_esm/tempo/index.js +21 -0
  24. package/_esm/tempo/index.js.map +1 -1
  25. package/_esm/version.js +1 -1
  26. package/_types/core/Base32.d.ts +79 -0
  27. package/_types/core/Base32.d.ts.map +1 -0
  28. package/_types/core/CompactSize.d.ts +107 -0
  29. package/_types/core/CompactSize.d.ts.map +1 -0
  30. package/_types/index.d.ts +48 -0
  31. package/_types/index.d.ts.map +1 -1
  32. package/_types/tempo/TempoAddress.d.ts +118 -0
  33. package/_types/tempo/TempoAddress.d.ts.map +1 -0
  34. package/_types/tempo/index.d.ts +21 -0
  35. package/_types/tempo/index.d.ts.map +1 -1
  36. package/_types/version.d.ts +1 -1
  37. package/core/Base32.ts +134 -0
  38. package/core/CompactSize.ts +169 -0
  39. package/index.ts +51 -0
  40. package/package.json +16 -1
  41. package/tempo/TempoAddress/package.json +6 -0
  42. package/tempo/TempoAddress.test.ts +202 -0
  43. package/tempo/TempoAddress.ts +225 -0
  44. package/tempo/index.ts +21 -3
  45. package/version.ts +1 -1
@@ -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/index.ts CHANGED
@@ -120,7 +120,27 @@ export * as PoolId from './PoolId.js'
120
120
  * @category Reference
121
121
  */
122
122
  export * as SignatureEnvelope from './SignatureEnvelope.js'
123
-
123
+ /**
124
+ * Tempo address encoding/decoding utilities for human-readable addresses.
125
+ *
126
+ * Tempo addresses use a bech32 base32-encoded format with `tempo1` prefix for mainnet
127
+ * and `tempoz1` prefix for zone addresses. Includes CompactSize zone ID encoding
128
+ * and double-SHA256 checksumming.
129
+ *
130
+ * @example
131
+ * ```ts twoslash
132
+ * import { TempoAddress } from 'ox/tempo'
133
+ *
134
+ * const encoded = TempoAddress.format('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28')
135
+ * // @log: 'tempo1wskntnrxxnq9x2f95wuyf0y7wk2l90fg8zd8djs'
136
+ *
137
+ * const { address, zoneId } = TempoAddress.parse(encoded)
138
+ * // @log: { address: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28' }
139
+ * ```
140
+ *
141
+ * @category Reference
142
+ */
143
+ export * as TempoAddress from './TempoAddress.js'
124
144
  /**
125
145
  * Tick-based pricing utilities for DEX price conversions.
126
146
  *
@@ -141,7 +161,6 @@ export * as SignatureEnvelope from './SignatureEnvelope.js'
141
161
  * @category Reference
142
162
  */
143
163
  export * as Tick from './Tick.js'
144
-
145
164
  /**
146
165
  * TIP-20 token ID utilities for converting between token IDs and addresses.
147
166
  *
@@ -163,7 +182,6 @@ export * as Tick from './Tick.js'
163
182
  * @category Reference
164
183
  */
165
184
  export * as TokenId from './TokenId.js'
166
-
167
185
  /**
168
186
  * Token role utilities for serializing role identifiers to keccak256 hashes.
169
187
  *
package/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = '0.13.1'
2
+ export const version = '0.13.2'