ox 1.0.3 → 1.0.5

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.
@@ -67,6 +67,28 @@ export * as AuthorizationTempo from './AuthorizationTempo.js';
67
67
  * @category Reference
68
68
  */
69
69
  export * as Channel from './Channel.js';
70
+ /**
71
+ * Tempo Earn `VaultAdapter` share math: raw vault-share and venue-share conversions
72
+ * at the anchor rate, the dilution-correct fee-share formula, and the
73
+ * `minimumOutput` slippage floor.
74
+ *
75
+ * Conversions are fee-blind mirrors of the adapter's anchor arithmetic; use the
76
+ * adapter's `previewRedeem` for user-facing value.
77
+ *
78
+ * @example
79
+ * ```ts twoslash
80
+ * import { EarnShares } from 'ox/tempo'
81
+ *
82
+ * const shareAmount = EarnShares.toAmount(
83
+ * { engineShares: 3n, shareSupply: 2n },
84
+ * 7n
85
+ * )
86
+ * // @log: 4n
87
+ * ```
88
+ *
89
+ * @category Reference
90
+ */
91
+ export * as EarnShares from './EarnShares.js';
70
92
  /**
71
93
  * Tempo key authorization utilities for provisioning and signing access keys.
72
94
  *
@@ -506,18 +528,17 @@ export * as VirtualMaster from './VirtualMaster.js';
506
528
  /**
507
529
  * Zone ID utilities for converting between zone IDs and zone chain IDs.
508
530
  *
509
- * Zone chain IDs are deterministically derived from zone IDs using the formula
510
- * `421_700_000 + zoneId`. This module provides helpers to convert between them.
531
+ * Zone chain IDs use the base and range assigned to their Tempo source chain.
511
532
  *
512
533
  * @example
513
534
  * ```ts twoslash
514
535
  * import { ZoneId } from 'ox/tempo'
515
536
  *
516
- * const zoneId = ZoneId.fromChainId(421_700_026)
517
- * // @log: 26
537
+ * const zoneId = ZoneId.fromChainId(421_700_001)
538
+ * // @log: 1
518
539
  *
519
- * const chainId = ZoneId.toChainId(26)
520
- * // @log: 421700026
540
+ * const chainId = ZoneId.toChainId(1)
541
+ * // @log: 421700001
521
542
  * ```
522
543
  *
523
544
  * @category Reference
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tempo/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,2DAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,oBAAoB,MAAM,2BAA2B,CAAA;AACjE;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AACvD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tempo/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,2DAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAC7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,oBAAoB,MAAM,2BAA2B,CAAA;AACjE;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AACvD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAA"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ox",
3
3
  "description": "Ethereum Standard Library",
4
4
  "type": "module",
5
- "version": "1.0.3",
5
+ "version": "1.0.5",
6
6
  "main": "./dist/index.js",
7
7
  "sideEffects": false,
8
8
  "license": "MIT",
@@ -520,6 +520,11 @@
520
520
  "types": "./dist/tempo/Channel.d.ts",
521
521
  "default": "./dist/tempo/Channel.js"
522
522
  },
523
+ "./tempo/EarnShares": {
524
+ "src": "./src/tempo/EarnShares.ts",
525
+ "types": "./dist/tempo/EarnShares.d.ts",
526
+ "default": "./dist/tempo/EarnShares.js"
527
+ },
523
528
  "./tempo/KeyAuthorization": {
524
529
  "src": "./src/tempo/KeyAuthorization.ts",
525
530
  "types": "./dist/tempo/KeyAuthorization.d.ts",
@@ -0,0 +1,120 @@
1
+ import { EarnShares } from 'ox/tempo'
2
+ import { describe, expect, test } from 'vite-plus/test'
3
+
4
+ const anchor = { engineShares: 3n, shareSupply: 2n } as const
5
+
6
+ describe('toAmount', () => {
7
+ test('default', () => {
8
+ expect(EarnShares.toAmount(anchor, 7n)).toBe(4n)
9
+ })
10
+
11
+ test('behavior: rounds down', () => {
12
+ expect(EarnShares.toAmount({ engineShares: 3n, shareSupply: 1n }, 2n)).toBe(
13
+ 0n,
14
+ )
15
+ })
16
+ })
17
+
18
+ describe('toAmountUp', () => {
19
+ test('default', () => {
20
+ expect(EarnShares.toAmountUp(anchor, 7n)).toBe(5n)
21
+ })
22
+
23
+ test('behavior: exact conversions do not round up', () => {
24
+ expect(EarnShares.toAmountUp(anchor, 3n)).toBe(2n)
25
+ })
26
+ })
27
+
28
+ describe('toVenueAmount', () => {
29
+ test('default', () => {
30
+ expect(EarnShares.toVenueAmount(anchor, 7n)).toBe(10n)
31
+ })
32
+
33
+ test('behavior: identity at the initial 1:1 anchor', () => {
34
+ expect(
35
+ EarnShares.toVenueAmount({ engineShares: 1n, shareSupply: 1n }, 12_345n),
36
+ ).toBe(12_345n)
37
+ })
38
+ })
39
+
40
+ describe('feeShares', () => {
41
+ test('default', () => {
42
+ expect(
43
+ EarnShares.feeShares({
44
+ activeAssets: 1_100n,
45
+ shareSupply: 1_000n,
46
+ totalFeeAssets: 100n,
47
+ }),
48
+ ).toBe(100n)
49
+ })
50
+
51
+ test('behavior: zero fee mints nothing', () => {
52
+ expect(
53
+ EarnShares.feeShares({
54
+ activeAssets: 1_100n,
55
+ shareSupply: 1_000n,
56
+ totalFeeAssets: 0n,
57
+ }),
58
+ ).toBe(0n)
59
+ })
60
+
61
+ test('behavior: fee at or above active assets mints nothing', () => {
62
+ expect(
63
+ EarnShares.feeShares({
64
+ activeAssets: 100n,
65
+ shareSupply: 1_000n,
66
+ totalFeeAssets: 100n,
67
+ }),
68
+ ).toBe(0n)
69
+ })
70
+
71
+ test('behavior: rounds down', () => {
72
+ expect(
73
+ EarnShares.feeShares({
74
+ activeAssets: 1_000n,
75
+ shareSupply: 999n,
76
+ totalFeeAssets: 100n,
77
+ }),
78
+ ).toBe(111n)
79
+ })
80
+ })
81
+
82
+ describe('minimumOutput', () => {
83
+ test('default', () => {
84
+ expect(EarnShares.minimumOutput(1_000_000n, 50)).toBe(995_000n)
85
+ })
86
+
87
+ test('behavior: zero slippage returns the expected output', () => {
88
+ expect(EarnShares.minimumOutput(1_000_000n, 0)).toBe(1_000_000n)
89
+ })
90
+
91
+ test('behavior: floors to 1n', () => {
92
+ expect(EarnShares.minimumOutput(1n, 9_999)).toBe(1n)
93
+ })
94
+
95
+ test('error: non-positive expected output', () => {
96
+ expect(() =>
97
+ EarnShares.minimumOutput(0n, 50),
98
+ ).toThrowErrorMatchingInlineSnapshot(
99
+ `[EarnShares.InvalidExpectedOutputError: Expected output \`0\` must be greater than zero.]`,
100
+ )
101
+ })
102
+
103
+ test('error: out-of-range slippage', () => {
104
+ expect(() => EarnShares.minimumOutput(1_000_000n, 10_000))
105
+ .toThrowErrorMatchingInlineSnapshot(`
106
+ [EarnShares.InvalidSlippageError: Slippage tolerance \`10000\` is invalid.
107
+
108
+ Slippage must be a whole number from 0 through 9999 basis points.]
109
+ `)
110
+ })
111
+
112
+ test('error: non-integer slippage', () => {
113
+ expect(() => EarnShares.minimumOutput(1_000_000n, 0.5))
114
+ .toThrowErrorMatchingInlineSnapshot(`
115
+ [EarnShares.InvalidSlippageError: Slippage tolerance \`0.5\` is invalid.
116
+
117
+ Slippage must be a whole number from 0 through 9999 basis points.]
118
+ `)
119
+ })
120
+ })
@@ -0,0 +1,235 @@
1
+ import * as Errors from '../core/Errors.js'
2
+
3
+ /** Basis-point denominator used by slippage bounds. */
4
+ export const basisPointScale = 10_000
5
+
6
+ /**
7
+ * Tempo Earn `VaultAdapter` conversion anchor.
8
+ *
9
+ * The adapter prices vault shares against venue shares through this pair:
10
+ * `engineShares` venue shares are worth `shareSupply` vault shares. It is initialised
11
+ * 1:1 and restated on `contribute` and `migrateEngine`.
12
+ *
13
+ * These conversions are raw and fee-blind; they ignore pending fee dilution
14
+ * and are unsuitable for user-facing value (use the adapter's `previewRedeem`).
15
+ */
16
+ export type Anchor = {
17
+ /** Venue shares held by the engine at the anchor point. */
18
+ engineShares: bigint
19
+ /** Vault share supply at the anchor point. */
20
+ shareSupply: bigint
21
+ }
22
+
23
+ /**
24
+ * Converts venue shares to a vault share amount at the anchor rate, rounding down.
25
+ *
26
+ * Mirrors `VaultAdapter.sharesToTokens`.
27
+ *
28
+ * @example
29
+ * ```ts twoslash
30
+ * import { EarnShares } from 'ox/tempo'
31
+ *
32
+ * const shareAmount = EarnShares.toAmount(
33
+ * { engineShares: 3n, shareSupply: 2n },
34
+ * 7n
35
+ * )
36
+ * // @log: 4n
37
+ * ```
38
+ *
39
+ * @param anchor - The conversion anchor.
40
+ * @param venueShareAmount - Venue share amount, base units.
41
+ * @returns Vault share amount, rounded down.
42
+ */
43
+ export function toAmount(anchor: Anchor, venueShareAmount: bigint): bigint {
44
+ return (venueShareAmount * anchor.shareSupply) / anchor.engineShares
45
+ }
46
+
47
+ export declare namespace toAmount {
48
+ type ErrorType = Errors.GlobalErrorType
49
+ }
50
+
51
+ /**
52
+ * Converts venue shares to a vault share amount at the anchor rate, rounding up.
53
+ *
54
+ * Mirrors the adapter's ceiling conversion used by exact-asset exits.
55
+ *
56
+ * @example
57
+ * ```ts twoslash
58
+ * import { EarnShares } from 'ox/tempo'
59
+ *
60
+ * const shareAmount = EarnShares.toAmountUp(
61
+ * { engineShares: 3n, shareSupply: 2n },
62
+ * 7n
63
+ * )
64
+ * // @log: 5n
65
+ * ```
66
+ *
67
+ * @param anchor - The conversion anchor.
68
+ * @param venueShareAmount - Venue share amount, base units.
69
+ * @returns Vault share amount, rounded up.
70
+ */
71
+ export function toAmountUp(anchor: Anchor, venueShareAmount: bigint): bigint {
72
+ const { engineShares, shareSupply } = anchor
73
+ return (venueShareAmount * shareSupply + engineShares - 1n) / engineShares
74
+ }
75
+
76
+ export declare namespace toAmountUp {
77
+ type ErrorType = Errors.GlobalErrorType
78
+ }
79
+
80
+ /**
81
+ * Converts a vault share amount to venue shares at the anchor rate, rounding down.
82
+ *
83
+ * Mirrors `VaultAdapter.tokensToShares`.
84
+ *
85
+ * @example
86
+ * ```ts twoslash
87
+ * import { EarnShares } from 'ox/tempo'
88
+ *
89
+ * const venueShareAmount = EarnShares.toVenueAmount(
90
+ * { engineShares: 3n, shareSupply: 2n },
91
+ * 7n
92
+ * )
93
+ * // @log: 10n
94
+ * ```
95
+ *
96
+ * @param anchor - The conversion anchor.
97
+ * @param shareAmount - Vault share amount, base units.
98
+ * @returns Venue share amount, rounded down.
99
+ */
100
+ export function toVenueAmount(anchor: Anchor, shareAmount: bigint): bigint {
101
+ return (shareAmount * anchor.engineShares) / anchor.shareSupply
102
+ }
103
+
104
+ export declare namespace toVenueAmount {
105
+ type ErrorType = Errors.GlobalErrorType
106
+ }
107
+
108
+ /**
109
+ * Computes the dilution-correct vault shares minted for an asset-denominated fee.
110
+ *
111
+ * Mirrors `FeeMath`:
112
+ * `feeShares = floor(fee * shareSupply / (activeAssets - fee))`, zero when the
113
+ * fee is zero or not smaller than the active assets. Minting this amount to the
114
+ * fee ledger prices the fee at post-mint value per share.
115
+ *
116
+ * @example
117
+ * ```ts twoslash
118
+ * import { EarnShares } from 'ox/tempo'
119
+ *
120
+ * const shares = EarnShares.feeShares({
121
+ * activeAssets: 1_100n,
122
+ * shareSupply: 1_000n,
123
+ * totalFeeAssets: 100n
124
+ * })
125
+ * // @log: 100n
126
+ * ```
127
+ *
128
+ * @param options - Fee accrual inputs.
129
+ * @returns Vault shares to mint for the fee, rounded down.
130
+ */
131
+ export function feeShares(options: feeShares.Options): bigint {
132
+ const { activeAssets, shareSupply, totalFeeAssets } = options
133
+ if (totalFeeAssets === 0n || totalFeeAssets >= activeAssets) return 0n
134
+ return (totalFeeAssets * shareSupply) / (activeAssets - totalFeeAssets)
135
+ }
136
+
137
+ export declare namespace feeShares {
138
+ export type Options = {
139
+ /** Assets backing the active (non-queued) supply, base units. */
140
+ activeAssets: bigint
141
+ /** Active vault share supply, base units. */
142
+ shareSupply: bigint
143
+ /** Total fee liability in asset units. */
144
+ totalFeeAssets: bigint
145
+ }
146
+ export type ErrorType = Errors.GlobalErrorType
147
+ }
148
+
149
+ /**
150
+ * Lowers an expected output by a basis-point slippage tolerance, flooring to `1n`.
151
+ *
152
+ * Suitable for lower bounds such as a deposit's minimum shares or a redeem's
153
+ * minimum assets; not for upper bounds such as an exact withdrawal's maximum
154
+ * shares.
155
+ *
156
+ * @example
157
+ * ```ts twoslash
158
+ * import { EarnShares } from 'ox/tempo'
159
+ *
160
+ * const minimumShares = EarnShares.minimumOutput(
161
+ * 1_000_000n,
162
+ * 50
163
+ * )
164
+ * // @log: 995_000n
165
+ * ```
166
+ *
167
+ * @param expectedAmount - Expected output in base units.
168
+ * @param slippageBps - Allowed slippage in basis points from `0` through `9_999`.
169
+ * @returns The minimum accepted output, floored to `1n`.
170
+ * @throws `InvalidExpectedOutputError` when `expectedAmount` is not positive.
171
+ * @throws `InvalidSlippageError` when `slippageBps` is outside its valid range.
172
+ */
173
+ export function minimumOutput(
174
+ expectedAmount: bigint,
175
+ slippageBps: number,
176
+ ): bigint {
177
+ if (expectedAmount <= 0n)
178
+ throw new InvalidExpectedOutputError({ expectedAmount })
179
+ if (
180
+ !Number.isInteger(slippageBps) ||
181
+ slippageBps < 0 ||
182
+ slippageBps >= basisPointScale
183
+ )
184
+ throw new InvalidSlippageError({ slippageBps })
185
+ const scale = BigInt(basisPointScale)
186
+ const bounded = (expectedAmount * (scale - BigInt(slippageBps))) / scale
187
+ return bounded === 0n ? 1n : bounded
188
+ }
189
+
190
+ export declare namespace minimumOutput {
191
+ type ErrorType =
192
+ | InvalidExpectedOutputError
193
+ | InvalidSlippageError
194
+ | Errors.GlobalErrorType
195
+ }
196
+
197
+ /**
198
+ * Error thrown when an expected output is not positive.
199
+ */
200
+ export class InvalidExpectedOutputError extends Errors.BaseError {
201
+ override readonly name = 'EarnShares.InvalidExpectedOutputError'
202
+
203
+ constructor(options: InvalidExpectedOutputError.Options) {
204
+ super(
205
+ `Expected output \`${options.expectedAmount}\` must be greater than zero.`,
206
+ )
207
+ }
208
+ }
209
+
210
+ export declare namespace InvalidExpectedOutputError {
211
+ export type Options = {
212
+ expectedAmount: bigint
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Error thrown when a slippage tolerance is not an integer from `0` through `9_999`.
218
+ */
219
+ export class InvalidSlippageError extends Errors.BaseError {
220
+ override readonly name = 'EarnShares.InvalidSlippageError'
221
+
222
+ constructor(options: InvalidSlippageError.Options) {
223
+ super(`Slippage tolerance \`${options.slippageBps}\` is invalid.`, {
224
+ metaMessages: [
225
+ `Slippage must be a whole number from 0 through ${basisPointScale - 1} basis points.`,
226
+ ],
227
+ })
228
+ }
229
+ }
230
+
231
+ export declare namespace InvalidSlippageError {
232
+ export type Options = {
233
+ slippageBps: number
234
+ }
235
+ }
@@ -0,0 +1,16 @@
1
+ import { expectTypeOf, test } from 'vp/test'
2
+ import * as ZoneId from './ZoneId.js'
3
+
4
+ test('accepts supported source IDs', () => {
5
+ expectTypeOf(ZoneId.fromChainId(421_700_001)).toEqualTypeOf<number>()
6
+ expectTypeOf(
7
+ ZoneId.fromChainId(1_424_310_001, 42_431),
8
+ ).toEqualTypeOf<number>()
9
+ expectTypeOf(ZoneId.toChainId(1)).toEqualTypeOf<number>()
10
+ expectTypeOf(ZoneId.toChainId(1, 42_431)).toEqualTypeOf<number>()
11
+
12
+ // @ts-expect-error Unsupported source ID.
13
+ ZoneId.fromChainId(421_700_001, 1)
14
+ // @ts-expect-error Unsupported source ID.
15
+ ZoneId.toChainId(1, 1)
16
+ })
@@ -3,40 +3,75 @@ import * as ZoneId from './ZoneId.js'
3
3
 
4
4
  describe('fromChainId', () => {
5
5
  test('default', () => {
6
- expect(ZoneId.fromChainId(4_217_000_006)).toBe(6)
6
+ expect(ZoneId.chainIdBase).toMatchInlineSnapshot(`421700000`)
7
+ expect(ZoneId.fromChainId(421_700_001)).toMatchInlineSnapshot(`1`)
7
8
  })
8
9
 
9
- test('zone 1', () => {
10
- expect(ZoneId.fromChainId(4_217_000_001)).toBe(1)
10
+ test('Moderato', () => {
11
+ expect(ZoneId.fromChainId(1_424_310_028, 42_431)).toMatchInlineSnapshot(
12
+ `28`,
13
+ )
11
14
  })
12
15
 
13
- test('zone 28', () => {
14
- expect(ZoneId.fromChainId(4_217_000_028)).toBe(28)
16
+ test('unsupported source id', () => {
17
+ expect(() =>
18
+ // @ts-expect-error Unsupported source ID.
19
+ ZoneId.fromChainId(421_700_001, 1),
20
+ ).toThrowErrorMatchingInlineSnapshot(`
21
+ [ZoneId.UnsupportedSourceIdError: Source chain ID "1" is not supported.
22
+
23
+ Supported source chain IDs: 4217, 42431.]
24
+ `)
15
25
  })
16
26
  })
17
27
 
18
28
  describe('toChainId', () => {
19
29
  test('default', () => {
20
- expect(ZoneId.toChainId(6)).toBe(4_217_000_006)
30
+ expect(ZoneId.toChainId(1)).toMatchInlineSnapshot(`421700001`)
31
+ })
32
+
33
+ test('Moderato', () => {
34
+ expect(ZoneId.toChainId(28, 42_431)).toMatchInlineSnapshot(`1424310028`)
21
35
  })
22
36
 
23
- test('zone 1', () => {
24
- expect(ZoneId.toChainId(1)).toBe(4_217_000_001)
37
+ test('wraps within the source chain range', () => {
38
+ expect(ZoneId.toChainId(1_002_610_001)).toMatchInlineSnapshot(`421700001`)
39
+ expect(ZoneId.toChainId(723_173_649, 42_431)).toMatchInlineSnapshot(
40
+ `1424310001`,
41
+ )
25
42
  })
26
43
 
27
- test('zone 28', () => {
28
- expect(ZoneId.toChainId(28)).toBe(4_217_000_028)
44
+ test('unsupported source id', () => {
45
+ expect(() =>
46
+ // @ts-expect-error Unsupported source ID.
47
+ ZoneId.toChainId(1, 1),
48
+ ).toThrowErrorMatchingInlineSnapshot(`
49
+ [ZoneId.UnsupportedSourceIdError: Source chain ID "1" is not supported.
50
+
51
+ Supported source chain IDs: 4217, 42431.]
52
+ `)
29
53
  })
30
54
  })
31
55
 
32
56
  describe('roundtrip', () => {
33
57
  test('fromChainId → toChainId', () => {
34
- const chainId = 4_217_000_006
35
- expect(ZoneId.toChainId(ZoneId.fromChainId(chainId))).toBe(chainId)
58
+ const chainId = 421_700_006
59
+ expect(ZoneId.toChainId(ZoneId.fromChainId(chainId))).toMatchInlineSnapshot(
60
+ `421700006`,
61
+ )
36
62
  })
37
63
 
38
64
  test('toChainId → fromChainId', () => {
39
65
  const zoneId = 42
40
- expect(ZoneId.fromChainId(ZoneId.toChainId(zoneId))).toBe(zoneId)
66
+ expect(ZoneId.fromChainId(ZoneId.toChainId(zoneId))).toMatchInlineSnapshot(
67
+ `42`,
68
+ )
69
+ })
70
+
71
+ test('Moderato', () => {
72
+ const zoneId = 42
73
+ const sourceId = 42_431
74
+ const chainId = ZoneId.toChainId(zoneId, sourceId)
75
+ expect(ZoneId.fromChainId(chainId, sourceId)).toMatchInlineSnapshot(`42`)
41
76
  })
42
77
  })
@@ -1,58 +1,94 @@
1
- import type * as Errors from '../core/Errors.js'
1
+ import * as Errors from '../core/Errors.js'
2
+
3
+ const chainIdConfig = {
4
+ 4_217: {
5
+ base: 421_700_000,
6
+ range: 1_002_610_000,
7
+ },
8
+ 42_431: {
9
+ base: 1_424_310_000,
10
+ range: 723_173_648,
11
+ },
12
+ } as const
13
+
14
+ const defaultSourceId = 4_217
2
15
 
3
16
  /**
4
- * Base offset for deriving zone chain IDs.
5
- *
6
- * Zone chain IDs are computed as `chainIdBase + zoneId`.
17
+ * Base offset for deriving Presto zone chain IDs.
7
18
  */
8
- export const chainIdBase = 4_217_000_000 as const
19
+ export const chainIdBase = chainIdConfig[defaultSourceId].base
20
+
21
+ /** Tempo source chain ID. */
22
+ export type SourceId = keyof typeof chainIdConfig
9
23
 
10
24
  /**
11
25
  * Derives a zone ID from a zone chain ID.
12
26
  *
13
- * Zone chain IDs follow the formula `4_217_000_000 + zoneId`, so a chain ID
14
- * of `4217000006` corresponds to zone ID `6`.
27
+ * Zone chain IDs use the base assigned to their Tempo source chain.
15
28
  *
16
29
  * @example
17
30
  * ```ts twoslash
18
31
  * import { ZoneId } from 'ox/tempo'
19
32
  *
20
- * const zoneId = ZoneId.fromChainId(4_217_000_006)
21
- * // @log: 6
33
+ * const zoneId = ZoneId.fromChainId(421_700_001)
34
+ * // @log: 1
22
35
  * ```
23
36
  *
24
37
  * @param chainId - The zone chain ID.
38
+ * @param sourceId - The Tempo source chain ID. Defaults to `4217` (Presto).
25
39
  * @returns The zone ID.
26
40
  */
27
- export function fromChainId(chainId: number): number {
28
- return chainId - chainIdBase
41
+ export function fromChainId(
42
+ chainId: number,
43
+ sourceId: SourceId = defaultSourceId,
44
+ ): number {
45
+ return chainId - getChainIdConfig(sourceId).base
29
46
  }
30
47
 
31
48
  export declare namespace fromChainId {
32
- type ErrorType = Errors.GlobalErrorType
49
+ type ErrorType = typeof UnsupportedSourceIdError | Errors.GlobalErrorType
33
50
  }
34
51
 
35
52
  /**
36
53
  * Derives a zone chain ID from a zone ID.
37
54
  *
38
- * Zone chain IDs follow the formula `4_217_000_000 + zoneId`, so zone ID
39
- * `6` corresponds to chain ID `4217000006`.
55
+ * Zone chain IDs use the base and range assigned to their Tempo source chain.
40
56
  *
41
57
  * @example
42
58
  * ```ts twoslash
43
59
  * import { ZoneId } from 'ox/tempo'
44
60
  *
45
- * const chainId = ZoneId.toChainId(6)
46
- * // @log: 4217000006
61
+ * const chainId = ZoneId.toChainId(1)
62
+ * // @log: 421700001
47
63
  * ```
48
64
  *
49
65
  * @param zoneId - The zone ID.
66
+ * @param sourceId - The Tempo source chain ID. Defaults to `4217` (Presto).
50
67
  * @returns The zone chain ID.
51
68
  */
52
- export function toChainId(zoneId: number): number {
53
- return chainIdBase + zoneId
69
+ export function toChainId(
70
+ zoneId: number,
71
+ sourceId: SourceId = defaultSourceId,
72
+ ): number {
73
+ const { base, range } = getChainIdConfig(sourceId)
74
+ return base + (zoneId % range)
54
75
  }
55
76
 
56
77
  export declare namespace toChainId {
57
- type ErrorType = Errors.GlobalErrorType
78
+ type ErrorType = typeof UnsupportedSourceIdError | Errors.GlobalErrorType
79
+ }
80
+
81
+ /** Thrown when a Tempo source chain ID is unsupported. */
82
+ export class UnsupportedSourceIdError extends Errors.BaseError {
83
+ override readonly name = 'ZoneId.UnsupportedSourceIdError'
84
+ constructor({ sourceId }: { sourceId: number }) {
85
+ super(`Source chain ID "${sourceId}" is not supported.`, {
86
+ metaMessages: ['Supported source chain IDs: 4217, 42431.'],
87
+ })
88
+ }
89
+ }
90
+
91
+ function getChainIdConfig(sourceId: number) {
92
+ if (sourceId === 4_217 || sourceId === 42_431) return chainIdConfig[sourceId]
93
+ throw new UnsupportedSourceIdError({ sourceId })
58
94
  }