@plasius/economy 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1628 @@
1
+ // src/errors.ts
2
+ var EconomyError = class extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = "EconomyError";
7
+ this.code = code;
8
+ }
9
+ };
10
+ function economyAssert(condition, code, message) {
11
+ if (!condition) {
12
+ throw new EconomyError(code, message);
13
+ }
14
+ }
15
+ function toEconomyErrorEnvelope(error, requestId) {
16
+ return {
17
+ schemaVersion: "1",
18
+ error: {
19
+ code: error.code,
20
+ message: error.message.slice(0, 256),
21
+ ...requestId === void 0 ? {} : { requestId }
22
+ }
23
+ };
24
+ }
25
+
26
+ // src/amount.ts
27
+ var TOKEN_SUBUNITS_PER_TOKEN = 1000n;
28
+ var TOKEN_SUBUNITS_MIN = -(2n ** 63n);
29
+ var TOKEN_SUBUNITS_MAX = 2n ** 63n - 1n;
30
+ var CANONICAL_INTEGER = /^(?:0|-?[1-9][0-9]*)$/u;
31
+ function assertInRange(amount) {
32
+ economyAssert(
33
+ amount >= TOKEN_SUBUNITS_MIN && amount <= TOKEN_SUBUNITS_MAX,
34
+ "AMOUNT_OUT_OF_RANGE",
35
+ "TokenSubunit amount is outside the signed 64-bit range"
36
+ );
37
+ }
38
+ function parseTokenSubunits(value) {
39
+ economyAssert(
40
+ CANONICAL_INTEGER.test(value),
41
+ "INVALID_AMOUNT",
42
+ "TokenSubunit amount must be a canonical base-10 integer string"
43
+ );
44
+ try {
45
+ const amount = BigInt(value);
46
+ assertInRange(amount);
47
+ return amount;
48
+ } catch (error) {
49
+ if (error instanceof EconomyError) {
50
+ throw error;
51
+ }
52
+ throw new EconomyError("INVALID_AMOUNT", "TokenSubunit amount is invalid");
53
+ }
54
+ }
55
+ function serializeTokenSubunits(amount) {
56
+ assertInRange(amount);
57
+ return amount.toString(10);
58
+ }
59
+ function wholeTokensToSubunits(tokens) {
60
+ const amount = tokens * TOKEN_SUBUNITS_PER_TOKEN;
61
+ assertInRange(amount);
62
+ return amount;
63
+ }
64
+ function isWholeTokenAmount(amount) {
65
+ return amount % TOKEN_SUBUNITS_PER_TOKEN === 0n;
66
+ }
67
+ function subunitsToWholeTokens(amount) {
68
+ economyAssert(
69
+ isWholeTokenAmount(amount),
70
+ "AMOUNT_NOT_WHOLE_TOKEN",
71
+ "Amount must contain whole Tokens"
72
+ );
73
+ return amount / TOKEN_SUBUNITS_PER_TOKEN;
74
+ }
75
+ function sumTokenSubunits(values) {
76
+ let total = 0n;
77
+ for (const value of values) {
78
+ total += parseTokenSubunits(value);
79
+ assertInRange(total);
80
+ }
81
+ return total;
82
+ }
83
+
84
+ // src/contracts.ts
85
+ var ECONOMY_CONTRACT_VERSION = "1";
86
+ var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
87
+ function assertEconomyIdentifier(value, label = "identifier") {
88
+ economyAssert(
89
+ IDENTIFIER.test(value),
90
+ "INVALID_CONTRACT",
91
+ `${label} must be a non-empty opaque identifier`
92
+ );
93
+ }
94
+ function parseIsoTimestamp(value) {
95
+ const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{3}))?Z$/u.exec(
96
+ value
97
+ );
98
+ economyAssert(
99
+ match !== null,
100
+ "INVALID_CONTRACT",
101
+ "Timestamp must be an ISO-8601 UTC value"
102
+ );
103
+ const milliseconds = Date.parse(value);
104
+ const base = match[1];
105
+ const fraction = match[2] ?? "000";
106
+ economyAssert(
107
+ Number.isFinite(milliseconds) && base !== void 0 && new Date(milliseconds).toISOString() === `${base}.${fraction}Z`,
108
+ "INVALID_CONTRACT",
109
+ "Timestamp must be valid"
110
+ );
111
+ return milliseconds;
112
+ }
113
+ function sortStringRecord(value) {
114
+ return Object.fromEntries(
115
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
116
+ );
117
+ }
118
+
119
+ // src/acquisition.ts
120
+ var BASELINE_GBP_REFERENCE_RATE = Object.freeze({
121
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
122
+ rateVersion: "gbp-reference-v1",
123
+ currency: "GBP",
124
+ referenceMinorUnitsPerToken: "10",
125
+ tokenSubunitsPerToken: TOKEN_SUBUNITS_PER_TOKEN.toString(10),
126
+ cashRedemptionAllowed: false
127
+ });
128
+ var BASELINE_GBP_CATALOG_VERSION = "gbp-v1";
129
+ var BASELINE_GBP_TOKEN_PACKS = Object.freeze([
130
+ Object.freeze({
131
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
132
+ packId: "gbp_5_50_v1",
133
+ catalogVersion: BASELINE_GBP_CATALOG_VERSION,
134
+ currency: "GBP",
135
+ priceMinorUnits: "500",
136
+ grantAmount: serializeTokenSubunits(50000n),
137
+ active: true
138
+ }),
139
+ Object.freeze({
140
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
141
+ packId: "gbp_10_100_v1",
142
+ catalogVersion: BASELINE_GBP_CATALOG_VERSION,
143
+ currency: "GBP",
144
+ priceMinorUnits: "1000",
145
+ grantAmount: serializeTokenSubunits(100000n),
146
+ active: true
147
+ }),
148
+ Object.freeze({
149
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
150
+ packId: "gbp_25_250_v1",
151
+ catalogVersion: BASELINE_GBP_CATALOG_VERSION,
152
+ currency: "GBP",
153
+ priceMinorUnits: "2500",
154
+ grantAmount: serializeTokenSubunits(250000n),
155
+ active: true
156
+ }),
157
+ Object.freeze({
158
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
159
+ packId: "gbp_50_500_v1",
160
+ catalogVersion: BASELINE_GBP_CATALOG_VERSION,
161
+ currency: "GBP",
162
+ priceMinorUnits: "5000",
163
+ grantAmount: serializeTokenSubunits(500000n),
164
+ active: true
165
+ })
166
+ ]);
167
+ var BASELINE_PURCHASE_LIMIT_POLICY = Object.freeze({
168
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
169
+ maxOrderPriceMinorUnits: "5000",
170
+ rollingPayerPriceMinorUnits: "10000",
171
+ rollingHouseholdPriceMinorUnits: "10000",
172
+ rollingDays: 30
173
+ });
174
+ function assertRewardConversion(conversion) {
175
+ economyAssert(
176
+ conversion.schemaVersion === ECONOMY_CONTRACT_VERSION,
177
+ "INVALID_CONTRACT",
178
+ "Unsupported reward-conversion contract version"
179
+ );
180
+ economyAssert(
181
+ conversion.provider === "ayet" || conversion.provider === "bitlabs",
182
+ "INVALID_CONTRACT",
183
+ "Unsupported reward-conversion provider"
184
+ );
185
+ assertEconomyIdentifier(conversion.providerEventId, "providerEventId");
186
+ assertEconomyIdentifier(
187
+ conversion.beneficiaryAccountId,
188
+ "beneficiaryAccountId"
189
+ );
190
+ assertEconomyIdentifier(conversion.walletId, "walletId");
191
+ economyAssert(
192
+ ["pending", "completed", "reconciled"].includes(conversion.status),
193
+ "INVALID_CONTRACT",
194
+ "Unsupported reward-conversion status"
195
+ );
196
+ economyAssert(
197
+ /^[A-Z]{3}$/u.test(conversion.providerCurrency),
198
+ "INVALID_CONTRACT",
199
+ "Reward payout currency must use an uppercase ISO code"
200
+ );
201
+ economyAssert(
202
+ parseNonNegativeInteger(
203
+ conversion.providerPayoutMinorUnits,
204
+ "Provider payout"
205
+ ) > 0n && parseNonNegativeInteger(conversion.gbpMinorUnits, "GBP payout") >= 0n && parseTokenSubunits(conversion.tokenSubunits) > 0n,
206
+ "INVALID_AMOUNT",
207
+ "Reward conversion must carry positive payout and Token values"
208
+ );
209
+ assertEconomyIdentifier(conversion.rateVersion, "rateVersion");
210
+ assertEconomyIdentifier(conversion.fxSnapshotId, "fxSnapshotId");
211
+ parseIsoTimestamp(conversion.occurredAt);
212
+ }
213
+ function parseNonNegativeInteger(value, label) {
214
+ economyAssert(
215
+ /^(?:0|[1-9][0-9]*)$/u.test(value),
216
+ "INVALID_AMOUNT",
217
+ `${label} must be a non-negative integer string`
218
+ );
219
+ return BigInt(value);
220
+ }
221
+ function assertTokenPack(pack) {
222
+ economyAssert(
223
+ pack.schemaVersion === ECONOMY_CONTRACT_VERSION,
224
+ "INVALID_CONTRACT",
225
+ "Unsupported Token pack contract version"
226
+ );
227
+ assertEconomyIdentifier(pack.packId, "packId");
228
+ assertEconomyIdentifier(pack.catalogVersion, "catalogVersion");
229
+ economyAssert(
230
+ pack.currency === "GBP" && typeof pack.active === "boolean",
231
+ "INVALID_CONTRACT",
232
+ "Token packs must use GBP and declare whether they are active"
233
+ );
234
+ economyAssert(
235
+ parseNonNegativeInteger(pack.priceMinorUnits, "Pack price") > 0n,
236
+ "INVALID_AMOUNT",
237
+ "Pack price must be positive"
238
+ );
239
+ const grant = parseTokenSubunits(pack.grantAmount);
240
+ economyAssert(
241
+ grant > 0n && isWholeTokenAmount(grant),
242
+ "AMOUNT_NOT_WHOLE_TOKEN",
243
+ "Paid pack grants must contain whole Tokens"
244
+ );
245
+ }
246
+ function assertTokenReferenceRate(rate) {
247
+ economyAssert(
248
+ rate.schemaVersion === ECONOMY_CONTRACT_VERSION,
249
+ "INVALID_CONTRACT",
250
+ "Unsupported Token reference-rate contract version"
251
+ );
252
+ assertEconomyIdentifier(rate.rateVersion, "rateVersion");
253
+ const referenceMinorUnits = parseNonNegativeInteger(
254
+ rate.referenceMinorUnitsPerToken,
255
+ "Reference minor units per Token"
256
+ );
257
+ const subunitsPerToken = parseNonNegativeInteger(
258
+ rate.tokenSubunitsPerToken,
259
+ "TokenSubunits per Token"
260
+ );
261
+ economyAssert(
262
+ rate.currency === "GBP" && referenceMinorUnits > 0n && subunitsPerToken === TOKEN_SUBUNITS_PER_TOKEN && rate.cashRedemptionAllowed === false,
263
+ "INVALID_CONTRACT",
264
+ "Token reference rate must preserve the GBP nominal, subunit, and no-redemption boundary"
265
+ );
266
+ }
267
+ function assertFlatTokenCatalog(packs, referenceRate) {
268
+ assertTokenReferenceRate(referenceRate);
269
+ economyAssert(
270
+ packs.length > 0,
271
+ "INVALID_CONTRACT",
272
+ "Token catalog must contain at least one pack"
273
+ );
274
+ const packIds = /* @__PURE__ */ new Set();
275
+ const catalogVersion = packs[0]?.catalogVersion;
276
+ const referenceMinorUnits = BigInt(
277
+ referenceRate.referenceMinorUnitsPerToken
278
+ );
279
+ const subunitsPerToken = BigInt(referenceRate.tokenSubunitsPerToken);
280
+ for (const pack of packs) {
281
+ assertTokenPack(pack);
282
+ economyAssert(
283
+ !packIds.has(pack.packId),
284
+ "DUPLICATE_IDENTIFIER",
285
+ "Token catalog pack IDs must be unique"
286
+ );
287
+ economyAssert(
288
+ pack.catalogVersion === catalogVersion,
289
+ "INVALID_CONTRACT",
290
+ "Token catalog packs must share one catalog version"
291
+ );
292
+ packIds.add(pack.packId);
293
+ const scaledReference = parseTokenSubunits(pack.grantAmount) * referenceMinorUnits;
294
+ economyAssert(
295
+ scaledReference % subunitsPerToken === 0n && scaledReference / subunitsPerToken === BigInt(pack.priceMinorUnits),
296
+ "INVALID_CONTRACT",
297
+ "Token catalog must use the declared flat nominal reference ratio"
298
+ );
299
+ }
300
+ }
301
+ function assertPurchaseLimitPolicy(policy) {
302
+ economyAssert(
303
+ policy.schemaVersion === ECONOMY_CONTRACT_VERSION,
304
+ "INVALID_CONTRACT",
305
+ "Unsupported purchase-limit contract version"
306
+ );
307
+ const maxOrder = parseNonNegativeInteger(
308
+ policy.maxOrderPriceMinorUnits,
309
+ "Maximum order"
310
+ );
311
+ const payer = parseNonNegativeInteger(
312
+ policy.rollingPayerPriceMinorUnits,
313
+ "Rolling payer limit"
314
+ );
315
+ const household = parseNonNegativeInteger(
316
+ policy.rollingHouseholdPriceMinorUnits,
317
+ "Rolling household limit"
318
+ );
319
+ economyAssert(
320
+ maxOrder > 0n && payer >= maxOrder && household >= maxOrder,
321
+ "INVALID_AMOUNT",
322
+ "Rolling purchase limits must cover at least one maximum order"
323
+ );
324
+ economyAssert(
325
+ Number.isSafeInteger(policy.rollingDays) && policy.rollingDays >= 1 && policy.rollingDays <= 365,
326
+ "INVALID_CONTRACT",
327
+ "Purchase-limit rolling window must be a bounded whole-day count"
328
+ );
329
+ }
330
+ function assertPurchaseIntentBinding(intent, pack) {
331
+ assertTokenPack(pack);
332
+ economyAssert(
333
+ intent.schemaVersion === ECONOMY_CONTRACT_VERSION,
334
+ "INVALID_CONTRACT",
335
+ "Unsupported purchase-intent contract version"
336
+ );
337
+ assertEconomyIdentifier(intent.intentId, "intentId");
338
+ assertEconomyIdentifier(intent.payerAccountId, "payerAccountId");
339
+ assertEconomyIdentifier(intent.receivingHouseholdId, "receivingHouseholdId");
340
+ assertEconomyIdentifier(intent.receivingWalletId, "receivingWalletId");
341
+ assertEconomyIdentifier(intent.packId, "packId");
342
+ assertEconomyIdentifier(intent.catalogVersion, "catalogVersion");
343
+ const createdAt = parseIsoTimestamp(intent.createdAt);
344
+ const expiresAt = parseIsoTimestamp(intent.expiresAt);
345
+ economyAssert(
346
+ expiresAt > createdAt,
347
+ "INVALID_TIME_WINDOW",
348
+ "Purchase intent expiry must follow its creation time"
349
+ );
350
+ economyAssert(
351
+ intent.expectedCurrency === "GBP" && intent.packId === pack.packId && intent.catalogVersion === pack.catalogVersion && intent.expectedCurrency === pack.currency && intent.expectedPriceMinorUnits === pack.priceMinorUnits && intent.grantAmount === pack.grantAmount,
352
+ "INVALID_CONTRACT",
353
+ "Purchase intent does not match its authoritative catalog pack"
354
+ );
355
+ parseNonNegativeInteger(intent.expectedPriceMinorUnits, "Expected price");
356
+ parseTokenSubunits(intent.grantAmount);
357
+ economyAssert(
358
+ [
359
+ "created",
360
+ "checkout-created",
361
+ "paid-unreconciled",
362
+ "credited",
363
+ "expired",
364
+ "cancelled",
365
+ "disputed"
366
+ ].includes(intent.status),
367
+ "INVALID_CONTRACT",
368
+ "Purchase intent has an unsupported status"
369
+ );
370
+ if (intent.providerCheckoutReferenceHash !== void 0) {
371
+ economyAssert(
372
+ /^sha256:[a-f0-9]{64}$/u.test(intent.providerCheckoutReferenceHash),
373
+ "INVALID_CONTRACT",
374
+ "Checkout references must be stored as sanitized SHA-256 hashes"
375
+ );
376
+ }
377
+ }
378
+ function assertOpenPurchaseIntent(intent, pack, checkedAt) {
379
+ assertPurchaseIntentBinding(intent, pack);
380
+ const checkedAtValue = parseIsoTimestamp(checkedAt);
381
+ economyAssert(
382
+ pack.active && (intent.status === "created" || intent.status === "checkout-created") && checkedAtValue >= parseIsoTimestamp(intent.createdAt) && checkedAtValue < parseIsoTimestamp(intent.expiresAt),
383
+ "INVALID_TIME_WINDOW",
384
+ "Purchase intent is inactive, expired, or not open for checkout"
385
+ );
386
+ }
387
+ function assertRewardRate(rate) {
388
+ economyAssert(
389
+ rate.schemaVersion === ECONOMY_CONTRACT_VERSION,
390
+ "INVALID_CONTRACT",
391
+ "Unsupported reward-rate contract version"
392
+ );
393
+ assertEconomyIdentifier(rate.rateVersion, "rateVersion");
394
+ economyAssert(
395
+ rate.provider === "ayet" || rate.provider === "bitlabs",
396
+ "INVALID_CONTRACT",
397
+ "Unsupported reward provider"
398
+ );
399
+ const effectiveFrom = parseIsoTimestamp(rate.effectiveFrom);
400
+ if (rate.effectiveUntil !== void 0) {
401
+ economyAssert(
402
+ parseIsoTimestamp(rate.effectiveUntil) > effectiveFrom,
403
+ "INVALID_TIME_WINDOW",
404
+ "Reward-rate expiry must follow its effective time"
405
+ );
406
+ }
407
+ }
408
+ function assertFxSnapshot(fx) {
409
+ economyAssert(
410
+ fx.schemaVersion === ECONOMY_CONTRACT_VERSION,
411
+ "INVALID_CONTRACT",
412
+ "Unsupported FX snapshot contract version"
413
+ );
414
+ assertEconomyIdentifier(fx.snapshotId, "snapshotId");
415
+ economyAssert(
416
+ /^[A-Z]{3}$/u.test(fx.sourceCurrency) && fx.quoteCurrency === "GBP",
417
+ "INVALID_CONTRACT",
418
+ "FX snapshots must convert an ISO currency into GBP"
419
+ );
420
+ parseIsoTimestamp(fx.capturedAt);
421
+ }
422
+ function convertRewardPayout(providerPayoutMinorUnits, rate, fx) {
423
+ assertRewardRate(rate);
424
+ assertFxSnapshot(fx);
425
+ const payout = parseNonNegativeInteger(
426
+ providerPayoutMinorUnits,
427
+ "Provider payout"
428
+ );
429
+ const fxNumerator = parseNonNegativeInteger(
430
+ fx.gbpMinorUnitsNumerator,
431
+ "FX numerator"
432
+ );
433
+ const fxDenominator = parseNonNegativeInteger(
434
+ fx.sourceMinorUnitsDenominator,
435
+ "FX denominator"
436
+ );
437
+ const tokenNumerator = parseNonNegativeInteger(
438
+ rate.tokenSubunitsNumerator,
439
+ "Rate numerator"
440
+ );
441
+ const tokenDenominator = parseNonNegativeInteger(
442
+ rate.gbpMinorUnitsDenominator,
443
+ "Rate denominator"
444
+ );
445
+ economyAssert(
446
+ fxDenominator > 0n && tokenDenominator > 0n,
447
+ "INVALID_AMOUNT",
448
+ "FX and reward-rate denominators must be positive"
449
+ );
450
+ economyAssert(
451
+ fxNumerator > 0n && tokenNumerator > 0n,
452
+ "INVALID_AMOUNT",
453
+ "FX and reward-rate numerators must be positive"
454
+ );
455
+ const gbpMinorUnits = payout * fxNumerator / fxDenominator;
456
+ const tokenSubunits = gbpMinorUnits * tokenNumerator / tokenDenominator;
457
+ return {
458
+ gbpMinorUnits: gbpMinorUnits.toString(10),
459
+ tokenSubunits: serializeTokenSubunits(tokenSubunits)
460
+ };
461
+ }
462
+ function mapBitLabsConversionState(state) {
463
+ const transitions = {
464
+ PENDING: {
465
+ conversionStatus: "pending",
466
+ activityType: "hold",
467
+ activityStatus: "held"
468
+ },
469
+ COMPLETED: {
470
+ conversionStatus: "completed",
471
+ activityType: "offerwall",
472
+ activityStatus: "settled"
473
+ },
474
+ RECONCILED: {
475
+ conversionStatus: "reconciled",
476
+ activityType: "reversal",
477
+ activityStatus: "reversed"
478
+ }
479
+ };
480
+ const transition = transitions[state];
481
+ economyAssert(
482
+ transition !== void 0,
483
+ "INVALID_CONTRACT",
484
+ "Unsupported BitLabs conversion state"
485
+ );
486
+ return transition;
487
+ }
488
+
489
+ // src/allocations.ts
490
+ function assertWholePositive(amount, label) {
491
+ const parsed = parseTokenSubunits(amount);
492
+ economyAssert(parsed > 0n, "INVALID_AMOUNT", `${label} must be positive`);
493
+ economyAssert(
494
+ isWholeTokenAmount(parsed),
495
+ "AMOUNT_NOT_WHOLE_TOKEN",
496
+ `${label} must contain whole Tokens`
497
+ );
498
+ return parsed;
499
+ }
500
+ function assertSlices(slices, expectedAmount, label) {
501
+ economyAssert(slices.length > 0, "INVALID_CONTRACT", `${label} are required`);
502
+ const lotIds = /* @__PURE__ */ new Set();
503
+ for (const slice of slices) {
504
+ assertEconomyIdentifier(slice.lotId, "lotId");
505
+ economyAssert(
506
+ !lotIds.has(slice.lotId),
507
+ "DUPLICATE_IDENTIFIER",
508
+ `${label} must not repeat a lot`
509
+ );
510
+ lotIds.add(slice.lotId);
511
+ economyAssert(
512
+ parseTokenSubunits(slice.amount) > 0n,
513
+ "INVALID_AMOUNT",
514
+ `${label} amounts must be positive`
515
+ );
516
+ }
517
+ economyAssert(
518
+ sumTokenSubunits(slices.map((slice) => slice.amount)) === expectedAmount,
519
+ "INVALID_CONTRACT",
520
+ `${label} must sum to the command amount`
521
+ );
522
+ }
523
+ function assertGameplayAllocation(allocation) {
524
+ economyAssert(
525
+ allocation.schemaVersion === ECONOMY_CONTRACT_VERSION,
526
+ "INVALID_CONTRACT",
527
+ "Unsupported allocation contract version"
528
+ );
529
+ assertEconomyIdentifier(allocation.allocationId, "allocationId");
530
+ assertEconomyIdentifier(allocation.householdId, "householdId");
531
+ assertEconomyIdentifier(allocation.hostWalletId, "hostWalletId");
532
+ assertEconomyIdentifier(allocation.childWalletId, "childWalletId");
533
+ assertEconomyIdentifier(allocation.childAccountId, "childAccountId");
534
+ const createdAt = parseIsoTimestamp(allocation.createdAt);
535
+ const updatedAt = parseIsoTimestamp(allocation.updatedAt);
536
+ economyAssert(
537
+ updatedAt >= createdAt,
538
+ "INVALID_TIME_WINDOW",
539
+ "Allocation update time cannot precede its creation time"
540
+ );
541
+ economyAssert(
542
+ allocation.status === "active" || allocation.status === "closed",
543
+ "INVALID_CONTRACT",
544
+ "Allocation has an unsupported status"
545
+ );
546
+ const reserved = parseTokenSubunits(allocation.reservedAmount);
547
+ const remaining = parseTokenSubunits(allocation.remainingAmount);
548
+ const reclaimed = parseTokenSubunits(allocation.reclaimedAmount);
549
+ economyAssert(
550
+ reserved > 0n && remaining >= 0n && reclaimed >= 0n && isWholeTokenAmount(reserved) && isWholeTokenAmount(remaining) && isWholeTokenAmount(reclaimed),
551
+ "AMOUNT_NOT_WHOLE_TOKEN",
552
+ "Allocation reservations and reclaims must contain whole Tokens"
553
+ );
554
+ economyAssert(
555
+ remaining + reclaimed <= reserved,
556
+ "INVALID_CONTRACT",
557
+ "Allocation remaining and reclaimed amounts exceed its reservation"
558
+ );
559
+ const fundingByLot = /* @__PURE__ */ new Map();
560
+ let funded = 0n;
561
+ for (const slice of allocation.fundingSlices) {
562
+ assertEconomyIdentifier(slice.lotId, "lotId");
563
+ const amount = parseTokenSubunits(slice.amount);
564
+ economyAssert(
565
+ amount > 0n,
566
+ "INVALID_AMOUNT",
567
+ "Allocation funding slices must be positive"
568
+ );
569
+ funded += amount;
570
+ fundingByLot.set(
571
+ slice.lotId,
572
+ (fundingByLot.get(slice.lotId) ?? 0n) + amount
573
+ );
574
+ }
575
+ economyAssert(
576
+ funded === reserved,
577
+ "INVALID_CONTRACT",
578
+ "Allocation funding provenance must equal its total reservation"
579
+ );
580
+ const reclaimedByLot = /* @__PURE__ */ new Map();
581
+ let reclaimedFromSlices = 0n;
582
+ for (const slice of allocation.reclaimedSlices) {
583
+ assertEconomyIdentifier(slice.lotId, "lotId");
584
+ const amount = parseTokenSubunits(slice.amount);
585
+ economyAssert(
586
+ amount > 0n,
587
+ "INVALID_AMOUNT",
588
+ "Allocation reclaim slices must be positive"
589
+ );
590
+ reclaimedFromSlices += amount;
591
+ reclaimedByLot.set(
592
+ slice.lotId,
593
+ (reclaimedByLot.get(slice.lotId) ?? 0n) + amount
594
+ );
595
+ }
596
+ economyAssert(
597
+ reclaimedFromSlices === reclaimed,
598
+ "INVALID_CONTRACT",
599
+ "Allocation reclaim provenance must equal its reclaimed amount"
600
+ );
601
+ for (const [lotId, amount] of reclaimedByLot) {
602
+ economyAssert(
603
+ amount <= (fundingByLot.get(lotId) ?? 0n),
604
+ "SOURCE_LOT_RESTRICTED",
605
+ "Reclaimed provenance cannot exceed allocation funding"
606
+ );
607
+ }
608
+ economyAssert(
609
+ allocation.version >= 1 && Number.isSafeInteger(allocation.version),
610
+ "INVALID_CONTRACT",
611
+ "Allocation version must be a positive safe integer"
612
+ );
613
+ const perTransaction = parseTokenSubunits(allocation.limits.perTransaction);
614
+ const periodic = parseTokenSubunits(allocation.limits.periodic);
615
+ economyAssert(
616
+ ["daily", "weekly", "monthly"].includes(allocation.limits.periodicWindow),
617
+ "INVALID_CONTRACT",
618
+ "Gameplay limit has an unsupported period"
619
+ );
620
+ economyAssert(
621
+ perTransaction >= 0n && periodic >= 0n && isWholeTokenAmount(perTransaction) && isWholeTokenAmount(periodic),
622
+ "AMOUNT_NOT_WHOLE_TOKEN",
623
+ "Gameplay limits must contain whole Tokens"
624
+ );
625
+ economyAssert(
626
+ perTransaction <= periodic || periodic === 0n,
627
+ "INVALID_CONTRACT",
628
+ "Per-transaction limit cannot exceed an enabled periodic limit"
629
+ );
630
+ }
631
+ function createGameplayAllocation(input) {
632
+ const amount = assertWholePositive(input.amount, "Allocation amount");
633
+ assertSlices(input.fundingSlices, amount, "Allocation funding slices");
634
+ const allocation = {
635
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
636
+ allocationId: input.allocationId,
637
+ householdId: input.householdId,
638
+ hostWalletId: input.hostWalletId,
639
+ childWalletId: input.childWalletId,
640
+ childAccountId: input.childAccountId,
641
+ status: "active",
642
+ reservedAmount: input.amount,
643
+ remainingAmount: input.amount,
644
+ reclaimedAmount: serializeTokenSubunits(0n),
645
+ fundingSlices: input.fundingSlices,
646
+ reclaimedSlices: [],
647
+ limits: input.limits,
648
+ version: 1,
649
+ createdAt: input.occurredAt,
650
+ updatedAt: input.occurredAt
651
+ };
652
+ assertGameplayAllocation(allocation);
653
+ return allocation;
654
+ }
655
+ function boostGameplayAllocation(allocation, input) {
656
+ assertGameplayAllocation(allocation);
657
+ economyAssert(
658
+ allocation.status === "active" && allocation.version === input.expectedVersion,
659
+ "INVALID_CONTRACT",
660
+ "Allocation is closed or its optimistic version is stale"
661
+ );
662
+ const amount = assertWholePositive(input.amount, "Boost amount");
663
+ assertSlices(input.fundingSlices, amount, "Boost funding slices");
664
+ const next = {
665
+ ...allocation,
666
+ reservedAmount: serializeTokenSubunits(
667
+ parseTokenSubunits(allocation.reservedAmount) + amount
668
+ ),
669
+ remainingAmount: serializeTokenSubunits(
670
+ parseTokenSubunits(allocation.remainingAmount) + amount
671
+ ),
672
+ fundingSlices: [...allocation.fundingSlices, ...input.fundingSlices],
673
+ version: allocation.version + 1,
674
+ updatedAt: input.occurredAt
675
+ };
676
+ assertGameplayAllocation(next);
677
+ return next;
678
+ }
679
+ function reclaimGameplayAllocation(allocation, input) {
680
+ assertGameplayAllocation(allocation);
681
+ economyAssert(
682
+ allocation.status === "active" && allocation.version === input.expectedVersion,
683
+ "INVALID_CONTRACT",
684
+ "Allocation is closed or its optimistic version is stale"
685
+ );
686
+ const amount = assertWholePositive(input.amount, "Reclaim amount");
687
+ economyAssert(
688
+ amount <= parseTokenSubunits(allocation.remainingAmount),
689
+ "INSUFFICIENT_BALANCE",
690
+ "Cannot reclaim more than the unused allocation"
691
+ );
692
+ assertSlices(input.sourceSlices, amount, "Reclaim source slices");
693
+ const fundedLotIds = new Set(
694
+ allocation.fundingSlices.map((slice) => slice.lotId)
695
+ );
696
+ economyAssert(
697
+ input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
698
+ "SOURCE_LOT_RESTRICTED",
699
+ "Reclaim slices must originate from the allocation"
700
+ );
701
+ const remaining = parseTokenSubunits(allocation.remainingAmount) - amount;
702
+ const next = {
703
+ ...allocation,
704
+ remainingAmount: serializeTokenSubunits(remaining),
705
+ reclaimedAmount: serializeTokenSubunits(
706
+ parseTokenSubunits(allocation.reclaimedAmount) + amount
707
+ ),
708
+ reclaimedSlices: [...allocation.reclaimedSlices, ...input.sourceSlices],
709
+ status: remaining === 0n ? "closed" : allocation.status,
710
+ version: allocation.version + 1,
711
+ updatedAt: input.occurredAt
712
+ };
713
+ assertGameplayAllocation(next);
714
+ return next;
715
+ }
716
+
717
+ // src/early-backers.ts
718
+ var PRE_UTILITY_BACKER_COHORT = "pre_utility_backer_v1";
719
+ function isWithinWindow(timestamp, launch, cutoff) {
720
+ const value = parseIsoTimestamp(timestamp);
721
+ return value >= launch && (cutoff === void 0 || value < cutoff);
722
+ }
723
+ function evaluateEarlyBacker(payerAccountId, receivingHouseholdId, paidLots, window, evaluatedAt) {
724
+ assertEconomyIdentifier(payerAccountId, "payerAccountId");
725
+ assertEconomyIdentifier(receivingHouseholdId, "receivingHouseholdId");
726
+ const launch = parseIsoTimestamp(window.publicTokensLaunchAt);
727
+ const cutoff = window.firstPublicSpendLiveAt === void 0 ? void 0 : parseIsoTimestamp(window.firstPublicSpendLiveAt);
728
+ economyAssert(
729
+ cutoff === void 0 || cutoff > launch,
730
+ "INVALID_TIME_WINDOW",
731
+ "First public spend must occur after public Token launch"
732
+ );
733
+ parseIsoTimestamp(evaluatedAt);
734
+ let retained = 0n;
735
+ const contributingLotIds = [];
736
+ const seenLotIds = /* @__PURE__ */ new Set();
737
+ for (const lot of paidLots) {
738
+ economyAssert(
739
+ lot.schemaVersion === ECONOMY_CONTRACT_VERSION,
740
+ "INVALID_CONTRACT",
741
+ "Unsupported paid-lot retention contract version"
742
+ );
743
+ assertEconomyIdentifier(lot.lotId, "lotId");
744
+ economyAssert(
745
+ !seenLotIds.has(lot.lotId),
746
+ "DUPLICATE_IDENTIFIER",
747
+ "Paid-lot retention input must not repeat a source lot"
748
+ );
749
+ seenLotIds.add(lot.lotId);
750
+ assertEconomyIdentifier(lot.payerAccountId, "payerAccountId");
751
+ assertEconomyIdentifier(lot.receivingHouseholdId, "receivingHouseholdId");
752
+ assertEconomyIdentifier(lot.purchaseId, "purchaseId");
753
+ assertEconomyIdentifier(lot.catalogVersion, "catalogVersion");
754
+ const purchasedAt = parseIsoTimestamp(lot.purchasedAt);
755
+ const settledAt = parseIsoTimestamp(lot.settledAt);
756
+ const creditedAt = parseIsoTimestamp(lot.creditedAt);
757
+ economyAssert(
758
+ purchasedAt <= settledAt && settledAt <= creditedAt,
759
+ "INVALID_TIME_WINDOW",
760
+ "Paid-lot timestamps must follow purchase, settlement, then credit order"
761
+ );
762
+ if (lot.payerAccountId !== payerAccountId || lot.receivingHouseholdId !== receivingHouseholdId) {
763
+ continue;
764
+ }
765
+ const inWindow = isWithinWindow(lot.purchasedAt, launch, cutoff) && isWithinWindow(lot.settledAt, launch, cutoff) && isWithinWindow(lot.creditedAt, launch, cutoff);
766
+ const amount = parseTokenSubunits(lot.retainedAmount);
767
+ economyAssert(
768
+ amount >= 0n,
769
+ "INVALID_AMOUNT",
770
+ "Retained paid-lot basis cannot be negative"
771
+ );
772
+ if (inWindow && amount > 0n) {
773
+ retained += amount;
774
+ contributingLotIds.push(lot.lotId);
775
+ }
776
+ }
777
+ return {
778
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
779
+ cohortKey: PRE_UTILITY_BACKER_COHORT,
780
+ status: retained > 0n ? "provisional" : "not-qualified",
781
+ payerAccountId,
782
+ receivingHouseholdId,
783
+ netRetainedAmount: serializeTokenSubunits(retained),
784
+ contributingLotIds: contributingLotIds.sort(
785
+ (left, right) => left.localeCompare(right)
786
+ ),
787
+ evaluatedAt
788
+ };
789
+ }
790
+
791
+ // src/ledger.ts
792
+ var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
793
+ "purchase",
794
+ "subscription",
795
+ "rewarded-ad",
796
+ "offerwall",
797
+ "allocation",
798
+ "boost",
799
+ "reclaim",
800
+ "spend",
801
+ "hold",
802
+ "refund",
803
+ "chargeback",
804
+ "adjustment",
805
+ "reversal",
806
+ "event",
807
+ "competition"
808
+ ]);
809
+ var ACTIVITY_STATUSES = /* @__PURE__ */ new Set([
810
+ "pending",
811
+ "held",
812
+ "settled",
813
+ "reversed",
814
+ "failed"
815
+ ]);
816
+ function assertCanonicalHash(value, label) {
817
+ economyAssert(
818
+ /^sha256:[a-f0-9]{64}$/u.test(value),
819
+ "INVALID_CONTRACT",
820
+ `${label} must be a canonical SHA-256 reference`
821
+ );
822
+ }
823
+ function hasControlCharacters(value) {
824
+ return [...value].some((character) => {
825
+ const code = character.charCodeAt(0);
826
+ return code <= 31 || code === 127;
827
+ });
828
+ }
829
+ function assertActivityEntry(entry) {
830
+ economyAssert(
831
+ entry.schemaVersion === ECONOMY_CONTRACT_VERSION,
832
+ "INVALID_CONTRACT",
833
+ "Unsupported activity-entry contract version"
834
+ );
835
+ assertEconomyIdentifier(entry.transactionId, "transactionId");
836
+ economyAssert(
837
+ ACTIVITY_TYPES.has(entry.activityType) && ACTIVITY_STATUSES.has(entry.status),
838
+ "INVALID_CONTRACT",
839
+ "Activity type or status is unsupported"
840
+ );
841
+ parseIsoTimestamp(entry.occurredAt);
842
+ economyAssert(
843
+ parseTokenSubunits(entry.amount) !== 0n,
844
+ "INVALID_AMOUNT",
845
+ "Activity entries cannot contain a zero amount"
846
+ );
847
+ if (entry.beneficiaryAccountId !== void 0) {
848
+ assertEconomyIdentifier(
849
+ entry.beneficiaryAccountId,
850
+ "beneficiaryAccountId"
851
+ );
852
+ }
853
+ if (entry.maskedReference !== void 0) {
854
+ economyAssert(
855
+ entry.maskedReference.length > 0 && entry.maskedReference.length <= 128 && !hasControlCharacters(entry.maskedReference),
856
+ "INVALID_CONTRACT",
857
+ "Masked activity references must be bounded and safe for display"
858
+ );
859
+ }
860
+ economyAssert(
861
+ entry.sourceLabel.trim().length > 0 && entry.sourceLabel.length <= 128 && !hasControlCharacters(entry.sourceLabel),
862
+ "INVALID_CONTRACT",
863
+ "Activity source labels must be bounded and safe for display"
864
+ );
865
+ }
866
+ function assertBalancedTransaction(transaction) {
867
+ economyAssert(
868
+ transaction.schemaVersion === ECONOMY_CONTRACT_VERSION,
869
+ "INVALID_CONTRACT",
870
+ "Unsupported ledger contract version"
871
+ );
872
+ assertEconomyIdentifier(transaction.transactionId, "transactionId");
873
+ assertEconomyIdentifier(transaction.idempotencyKey, "idempotencyKey");
874
+ economyAssert(
875
+ ACTIVITY_TYPES.has(transaction.activityType) && ACTIVITY_STATUSES.has(transaction.status),
876
+ "INVALID_CONTRACT",
877
+ "Transaction activity type or status is unsupported"
878
+ );
879
+ if (transaction.providerEventId !== void 0) {
880
+ assertEconomyIdentifier(transaction.providerEventId, "providerEventId");
881
+ }
882
+ if (transaction.reversesTransactionId !== void 0) {
883
+ assertEconomyIdentifier(
884
+ transaction.reversesTransactionId,
885
+ "reversesTransactionId"
886
+ );
887
+ }
888
+ economyAssert(
889
+ transaction.activityType === "reversal" === (transaction.reversesTransactionId !== void 0),
890
+ "INVALID_CONTRACT",
891
+ "Reversal transactions must identify exactly one original transaction"
892
+ );
893
+ const effectiveAt = parseIsoTimestamp(transaction.effectiveAt);
894
+ const recordedAt = parseIsoTimestamp(transaction.recordedAt);
895
+ economyAssert(
896
+ recordedAt >= effectiveAt,
897
+ "INVALID_TIME_WINDOW",
898
+ "Transaction recording time cannot precede its effective time"
899
+ );
900
+ if (transaction.previousCanonicalHash !== void 0) {
901
+ assertCanonicalHash(
902
+ transaction.previousCanonicalHash,
903
+ "Previous canonical hash"
904
+ );
905
+ }
906
+ if (transaction.canonicalHash !== void 0) {
907
+ assertCanonicalHash(transaction.canonicalHash, "Canonical hash");
908
+ }
909
+ const metadataEntries = Object.entries(transaction.metadata);
910
+ economyAssert(
911
+ metadataEntries.length <= 32 && metadataEntries.every(
912
+ ([key, value]) => /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u.test(key) && value.length <= 512 && !hasControlCharacters(value)
913
+ ),
914
+ "INVALID_CONTRACT",
915
+ "Transaction metadata must be bounded and free of control characters"
916
+ );
917
+ economyAssert(
918
+ transaction.postings.length >= 2,
919
+ "UNBALANCED_TRANSACTION",
920
+ "A ledger transaction needs at least two postings"
921
+ );
922
+ const postingIds = /* @__PURE__ */ new Set();
923
+ for (const posting of transaction.postings) {
924
+ economyAssert(
925
+ posting.schemaVersion === ECONOMY_CONTRACT_VERSION,
926
+ "INVALID_CONTRACT",
927
+ "Unsupported posting contract version"
928
+ );
929
+ assertEconomyIdentifier(posting.postingId, "postingId");
930
+ assertEconomyIdentifier(posting.accountId, "accountId");
931
+ if (posting.walletId !== void 0) {
932
+ assertEconomyIdentifier(posting.walletId, "walletId");
933
+ }
934
+ if (posting.lotId !== void 0) {
935
+ assertEconomyIdentifier(posting.lotId, "lotId");
936
+ }
937
+ economyAssert(
938
+ posting.transactionId === transaction.transactionId,
939
+ "INVALID_CONTRACT",
940
+ "Posting transactionId must match its transaction"
941
+ );
942
+ economyAssert(
943
+ !postingIds.has(posting.postingId),
944
+ "DUPLICATE_IDENTIFIER",
945
+ "Posting identifiers must be unique within a transaction"
946
+ );
947
+ postingIds.add(posting.postingId);
948
+ economyAssert(
949
+ parseTokenSubunits(posting.amount) !== 0n,
950
+ "INVALID_AMOUNT",
951
+ "Zero-value postings are not permitted"
952
+ );
953
+ }
954
+ economyAssert(
955
+ sumTokenSubunits(transaction.postings.map((posting) => posting.amount)) === 0n,
956
+ "UNBALANCED_TRANSACTION",
957
+ "Ledger postings must sum to zero TokenSubunits"
958
+ );
959
+ }
960
+ function canonicalTransactionPayload(transaction) {
961
+ assertBalancedTransaction(transaction);
962
+ const postings = [...transaction.postings].sort((left, right) => left.postingId.localeCompare(right.postingId)).map((posting) => ({
963
+ schemaVersion: posting.schemaVersion,
964
+ postingId: posting.postingId,
965
+ transactionId: posting.transactionId,
966
+ accountId: posting.accountId,
967
+ ...posting.walletId === void 0 ? {} : { walletId: posting.walletId },
968
+ ...posting.lotId === void 0 ? {} : { lotId: posting.lotId },
969
+ amount: posting.amount
970
+ }));
971
+ return JSON.stringify({
972
+ schemaVersion: transaction.schemaVersion,
973
+ transactionId: transaction.transactionId,
974
+ activityType: transaction.activityType,
975
+ status: transaction.status,
976
+ idempotencyKey: transaction.idempotencyKey,
977
+ ...transaction.providerEventId === void 0 ? {} : { providerEventId: transaction.providerEventId },
978
+ ...transaction.reversesTransactionId === void 0 ? {} : { reversesTransactionId: transaction.reversesTransactionId },
979
+ effectiveAt: transaction.effectiveAt,
980
+ recordedAt: transaction.recordedAt,
981
+ ...transaction.previousCanonicalHash === void 0 ? {} : { previousCanonicalHash: transaction.previousCanonicalHash },
982
+ metadata: sortStringRecord(transaction.metadata),
983
+ postings
984
+ });
985
+ }
986
+ function createReversalTransaction(original, input) {
987
+ assertBalancedTransaction(original);
988
+ economyAssert(
989
+ original.reversesTransactionId === void 0,
990
+ "INVALID_CONTRACT",
991
+ "A reversal cannot itself be reversed by this helper"
992
+ );
993
+ economyAssert(
994
+ input.postingIds.length === original.postings.length,
995
+ "INVALID_CONTRACT",
996
+ "A reversal needs one new posting identifier per original posting"
997
+ );
998
+ const postings = original.postings.map((posting, index) => {
999
+ const postingId = input.postingIds[index];
1000
+ economyAssert(
1001
+ postingId !== void 0,
1002
+ "INVALID_CONTRACT",
1003
+ "Missing reversal posting identifier"
1004
+ );
1005
+ return {
1006
+ ...posting,
1007
+ postingId,
1008
+ transactionId: input.transactionId,
1009
+ amount: serializeTokenSubunits(-parseTokenSubunits(posting.amount))
1010
+ };
1011
+ });
1012
+ const transaction = {
1013
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1014
+ transactionId: input.transactionId,
1015
+ activityType: "reversal",
1016
+ status: "settled",
1017
+ idempotencyKey: input.idempotencyKey,
1018
+ reversesTransactionId: original.transactionId,
1019
+ effectiveAt: input.effectiveAt,
1020
+ recordedAt: input.recordedAt,
1021
+ ...input.previousCanonicalHash === void 0 ? {} : { previousCanonicalHash: input.previousCanonicalHash },
1022
+ metadata: input.metadata ?? {},
1023
+ postings
1024
+ };
1025
+ assertBalancedTransaction(transaction);
1026
+ return transaction;
1027
+ }
1028
+ function assertReversalAvailable(originalTransactionId, existingTransactions) {
1029
+ assertEconomyIdentifier(originalTransactionId, "originalTransactionId");
1030
+ economyAssert(
1031
+ !existingTransactions.some(
1032
+ (transaction) => transaction.reversesTransactionId === originalTransactionId && transaction.status !== "failed"
1033
+ ),
1034
+ "REVERSAL_ALREADY_EXISTS",
1035
+ "The transaction already has an effective reversal"
1036
+ );
1037
+ }
1038
+
1039
+ // src/lots.ts
1040
+ function assertSourceLot(lot) {
1041
+ economyAssert(
1042
+ lot.schemaVersion === ECONOMY_CONTRACT_VERSION,
1043
+ "INVALID_CONTRACT",
1044
+ "Unsupported source-lot contract version"
1045
+ );
1046
+ assertEconomyIdentifier(lot.lotId, "lotId");
1047
+ assertEconomyIdentifier(lot.walletId, "walletId");
1048
+ assertEconomyIdentifier(lot.beneficiaryAccountId, "beneficiaryAccountId");
1049
+ if (lot.householdId !== void 0) {
1050
+ assertEconomyIdentifier(lot.householdId, "householdId");
1051
+ }
1052
+ if (lot.payerAccountId !== void 0) {
1053
+ assertEconomyIdentifier(lot.payerAccountId, "payerAccountId");
1054
+ }
1055
+ if (lot.providerEventId !== void 0) {
1056
+ assertEconomyIdentifier(lot.providerEventId, "providerEventId");
1057
+ }
1058
+ assertEconomyIdentifier(lot.rateVersion, "rateVersion");
1059
+ economyAssert(
1060
+ [
1061
+ "shopify",
1062
+ "ayet",
1063
+ "bitlabs",
1064
+ "subscription",
1065
+ "event",
1066
+ "competition",
1067
+ "adjustment"
1068
+ ].includes(lot.source),
1069
+ "INVALID_CONTRACT",
1070
+ "Source lot has an unsupported source"
1071
+ );
1072
+ economyAssert(
1073
+ ["household-allocatable", "same-user-only", "non-transferable"].includes(
1074
+ lot.transferPolicy
1075
+ ),
1076
+ "INVALID_CONTRACT",
1077
+ "Source lot has an unsupported transfer policy"
1078
+ );
1079
+ economyAssert(
1080
+ ["none", "partial", "refunded", "disputed", "chargeback-lost"].includes(
1081
+ lot.refundState
1082
+ ),
1083
+ "INVALID_CONTRACT",
1084
+ "Source lot has an unsupported refund state"
1085
+ );
1086
+ if (lot.source === "ayet" || lot.source === "bitlabs") {
1087
+ economyAssert(
1088
+ lot.transferPolicy === "same-user-only" && lot.providerEventId !== void 0,
1089
+ "SOURCE_LOT_RESTRICTED",
1090
+ "Reward-provider lots must remain with the earning account"
1091
+ );
1092
+ }
1093
+ if (lot.source === "shopify") {
1094
+ economyAssert(
1095
+ lot.transferPolicy === "household-allocatable" && lot.householdId !== void 0 && lot.payerAccountId !== void 0,
1096
+ "INVALID_CONTRACT",
1097
+ "Shopify lots must retain payer and receiving-household provenance"
1098
+ );
1099
+ }
1100
+ economyAssert(
1101
+ /^sha256:[a-f0-9]{64}$/u.test(lot.settlementEvidenceHash),
1102
+ "INVALID_CONTRACT",
1103
+ "Settlement evidence must be a sanitized SHA-256 reference"
1104
+ );
1105
+ const settledAt = parseIsoTimestamp(lot.settledAt);
1106
+ const creditedAt = parseIsoTimestamp(lot.creditedAt);
1107
+ economyAssert(
1108
+ creditedAt >= settledAt,
1109
+ "INVALID_TIME_WINDOW",
1110
+ "Source-lot credit time cannot precede settlement"
1111
+ );
1112
+ if (lot.cohortKey !== void 0) {
1113
+ assertEconomyIdentifier(lot.cohortKey, "cohortKey");
1114
+ }
1115
+ const original = parseTokenSubunits(lot.originalAmount);
1116
+ const remaining = parseTokenSubunits(lot.remainingAmount);
1117
+ const held = parseTokenSubunits(lot.heldAmount);
1118
+ const reversed = parseTokenSubunits(lot.reversedAmount);
1119
+ economyAssert(
1120
+ original > 0n && remaining >= 0n && held >= 0n && reversed >= 0n,
1121
+ "INVALID_AMOUNT",
1122
+ "Source-lot amounts must be non-negative and original must be positive"
1123
+ );
1124
+ economyAssert(
1125
+ remaining <= original && held <= remaining && reversed <= original,
1126
+ "INVALID_CONTRACT",
1127
+ "Source-lot amount components exceed the original or remaining amount"
1128
+ );
1129
+ economyAssert(
1130
+ remaining + reversed <= original,
1131
+ "INVALID_CONTRACT",
1132
+ "Remaining and reversed source-lot amounts exceed the original"
1133
+ );
1134
+ }
1135
+ function availableSourceLotAmount(lot) {
1136
+ assertSourceLot(lot);
1137
+ return parseTokenSubunits(lot.remainingAmount) - parseTokenSubunits(lot.heldAmount);
1138
+ }
1139
+ function canUseSourceLot(lot, context) {
1140
+ if (context.operation === "refund") {
1141
+ return true;
1142
+ }
1143
+ if (context.operation === "allocate" || context.operation === "reclaim") {
1144
+ return lot.transferPolicy === "household-allocatable" && lot.householdId !== void 0 && lot.householdId === context.householdId;
1145
+ }
1146
+ if (lot.transferPolicy === "household-allocatable") {
1147
+ return lot.householdId !== void 0 && lot.householdId === context.householdId;
1148
+ }
1149
+ return lot.beneficiaryAccountId === context.beneficiaryAccountId;
1150
+ }
1151
+ function selectSourceLots(lots, requestedAmount, context) {
1152
+ const requested = parseTokenSubunits(requestedAmount);
1153
+ economyAssert(
1154
+ requested > 0n,
1155
+ "INVALID_AMOUNT",
1156
+ "Requested source-lot amount must be positive"
1157
+ );
1158
+ assertEconomyIdentifier(context.beneficiaryAccountId, "beneficiaryAccountId");
1159
+ const ordered = [...lots].sort((left, right) => {
1160
+ const timeDifference = parseIsoTimestamp(left.creditedAt) - parseIsoTimestamp(right.creditedAt);
1161
+ return timeDifference === 0 ? left.lotId.localeCompare(right.lotId) : timeDifference;
1162
+ });
1163
+ let allAvailable = 0n;
1164
+ let remaining = requested;
1165
+ const slices = [];
1166
+ for (const lot of ordered) {
1167
+ const available = availableSourceLotAmount(lot);
1168
+ allAvailable += available;
1169
+ if (available === 0n || !canUseSourceLot(lot, context)) {
1170
+ continue;
1171
+ }
1172
+ const selected = available < remaining ? available : remaining;
1173
+ slices.push({ lotId: lot.lotId, amount: serializeTokenSubunits(selected) });
1174
+ remaining -= selected;
1175
+ if (remaining === 0n) {
1176
+ return slices;
1177
+ }
1178
+ }
1179
+ if (allAvailable >= requested) {
1180
+ throw new EconomyError(
1181
+ "SOURCE_LOT_RESTRICTED",
1182
+ "Available source lots do not permit this operation"
1183
+ );
1184
+ }
1185
+ throw new EconomyError(
1186
+ "INSUFFICIENT_ELIGIBLE_LOTS",
1187
+ "Insufficient eligible source-lot balance"
1188
+ );
1189
+ }
1190
+
1191
+ // src/operations.ts
1192
+ function assertDistinctAdjustmentApproval(request) {
1193
+ economyAssert(
1194
+ request.schemaVersion === ECONOMY_CONTRACT_VERSION,
1195
+ "INVALID_CONTRACT",
1196
+ "Unsupported adjustment contract version"
1197
+ );
1198
+ assertEconomyIdentifier(request.adjustmentId, "adjustmentId");
1199
+ assertEconomyIdentifier(request.walletId, "walletId");
1200
+ assertEconomyIdentifier(request.initiatedByAccountId, "initiatedByAccountId");
1201
+ economyAssert(
1202
+ parseTokenSubunits(request.amount) !== 0n,
1203
+ "INVALID_AMOUNT",
1204
+ "Adjustment amount cannot be zero"
1205
+ );
1206
+ economyAssert(
1207
+ request.reason.trim().length > 0 && request.reason.length <= 512,
1208
+ "INVALID_CONTRACT",
1209
+ "Adjustment reason is required and must be bounded"
1210
+ );
1211
+ assertEconomyIdentifier(request.ticketReference, "ticketReference");
1212
+ const initiatedAt = parseIsoTimestamp(request.initiatedAt);
1213
+ economyAssert(
1214
+ request.approvedByAccountId !== void 0 && request.approvedByAccountId !== request.initiatedByAccountId,
1215
+ "INVALID_CONTRACT",
1216
+ "Adjustment approval requires a distinct authorized operator"
1217
+ );
1218
+ assertEconomyIdentifier(request.approvedByAccountId, "approvedByAccountId");
1219
+ economyAssert(
1220
+ request.approvedAt !== void 0 && parseIsoTimestamp(request.approvedAt) >= initiatedAt,
1221
+ "INVALID_TIME_WINDOW",
1222
+ "Adjustment approval time must follow initiation"
1223
+ );
1224
+ economyAssert(
1225
+ request.status === "approved" || request.status === "executed",
1226
+ "INVALID_CONTRACT",
1227
+ "Only approved adjustments satisfy dual-control approval"
1228
+ );
1229
+ if (request.resultingTransactionId !== void 0) {
1230
+ assertEconomyIdentifier(
1231
+ request.resultingTransactionId,
1232
+ "resultingTransactionId"
1233
+ );
1234
+ }
1235
+ }
1236
+
1237
+ // src/projection.ts
1238
+ function applyTransactionToProjection(current, transaction) {
1239
+ assertBalancedTransaction(transaction);
1240
+ const next = { ...current };
1241
+ for (const posting of transaction.postings) {
1242
+ const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
1243
+ next[posting.accountId] = serializeTokenSubunits(
1244
+ prior + parseTokenSubunits(posting.amount)
1245
+ );
1246
+ }
1247
+ return next;
1248
+ }
1249
+ function rebuildBalanceProjection(transactions) {
1250
+ let projection = {};
1251
+ const transactionIds = /* @__PURE__ */ new Set();
1252
+ const idempotencyKeys = /* @__PURE__ */ new Set();
1253
+ const providerEvents = /* @__PURE__ */ new Set();
1254
+ for (const transaction of transactions) {
1255
+ economyAssert(
1256
+ !transactionIds.has(transaction.transactionId),
1257
+ "DUPLICATE_TRANSACTION",
1258
+ "Transaction identifiers must be unique"
1259
+ );
1260
+ economyAssert(
1261
+ !idempotencyKeys.has(transaction.idempotencyKey),
1262
+ "DUPLICATE_TRANSACTION",
1263
+ "Idempotency keys must be unique"
1264
+ );
1265
+ if (transaction.providerEventId !== void 0) {
1266
+ economyAssert(
1267
+ !providerEvents.has(transaction.providerEventId),
1268
+ "DUPLICATE_TRANSACTION",
1269
+ "Provider event identifiers must be unique"
1270
+ );
1271
+ providerEvents.add(transaction.providerEventId);
1272
+ }
1273
+ transactionIds.add(transaction.transactionId);
1274
+ idempotencyKeys.add(transaction.idempotencyKey);
1275
+ projection = applyTransactionToProjection(projection, transaction);
1276
+ }
1277
+ return projection;
1278
+ }
1279
+ function assertNonNegativeAccounts(projection, protectedAccountIds) {
1280
+ for (const accountId of protectedAccountIds) {
1281
+ economyAssert(
1282
+ parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
1283
+ "NEGATIVE_PROJECTION",
1284
+ "A protected economy account would become negative"
1285
+ );
1286
+ }
1287
+ }
1288
+ function createProjectionSnapshot(transactions, rebuiltAt) {
1289
+ parseIsoTimestamp(rebuiltAt);
1290
+ const last = transactions.at(-1);
1291
+ return {
1292
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1293
+ balances: rebuildBalanceProjection(transactions),
1294
+ ...last === void 0 ? {} : { lastTransactionId: last.transactionId },
1295
+ rebuiltAt
1296
+ };
1297
+ }
1298
+
1299
+ // src/recurrence.ts
1300
+ var BASELINE_MONTHLY_SUBSCRIPTION_PLAN = Object.freeze({
1301
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1302
+ planId: "monthly-10-100-v1",
1303
+ catalogVersion: "gbp-subscriptions-v1",
1304
+ interval: "monthly",
1305
+ priceMinorUnits: "1000",
1306
+ currency: "GBP",
1307
+ grantAmount: serializeTokenSubunits(100000n),
1308
+ enabled: false,
1309
+ effectiveFrom: "2026-07-15T00:00:00.000Z"
1310
+ });
1311
+ function assertTokenSubscriptionPlan(plan) {
1312
+ economyAssert(
1313
+ plan.schemaVersion === ECONOMY_CONTRACT_VERSION,
1314
+ "INVALID_CONTRACT",
1315
+ "Unsupported subscription-plan contract version"
1316
+ );
1317
+ assertEconomyIdentifier(plan.planId, "planId");
1318
+ assertEconomyIdentifier(plan.catalogVersion, "catalogVersion");
1319
+ economyAssert(
1320
+ ["daily", "weekly", "monthly"].includes(plan.interval) && plan.currency === "GBP" && typeof plan.enabled === "boolean",
1321
+ "INVALID_CONTRACT",
1322
+ "Subscription recurrence, currency, or enabled state is invalid"
1323
+ );
1324
+ economyAssert(
1325
+ /^(?:0|[1-9][0-9]*)$/u.test(plan.priceMinorUnits) && BigInt(plan.priceMinorUnits) > 0n,
1326
+ "INVALID_AMOUNT",
1327
+ "Subscription price must be a positive integer minor-unit string"
1328
+ );
1329
+ const grantAmount = parseTokenSubunits(plan.grantAmount);
1330
+ economyAssert(
1331
+ grantAmount > 0n && isWholeTokenAmount(grantAmount),
1332
+ "AMOUNT_NOT_WHOLE_TOKEN",
1333
+ "Subscription grants must contain whole Tokens"
1334
+ );
1335
+ parseIsoTimestamp(plan.effectiveFrom);
1336
+ }
1337
+
1338
+ // src/spend-requests.ts
1339
+ function assertSpendRequest(request) {
1340
+ economyAssert(
1341
+ request.schemaVersion === ECONOMY_CONTRACT_VERSION,
1342
+ "INVALID_CONTRACT",
1343
+ "Unsupported spend-request contract version"
1344
+ );
1345
+ assertEconomyIdentifier(request.requestId, "requestId");
1346
+ assertEconomyIdentifier(request.childAccountId, "childAccountId");
1347
+ assertEconomyIdentifier(request.allocationId, "allocationId");
1348
+ economyAssert(
1349
+ request.purpose === "gameplay-conversion" || request.purpose === "non-gameplay",
1350
+ "INVALID_CONTRACT",
1351
+ "Spend request has an unsupported purpose"
1352
+ );
1353
+ economyAssert(
1354
+ [
1355
+ "requested",
1356
+ "approved",
1357
+ "rejected",
1358
+ "expired",
1359
+ "executed",
1360
+ "cancelled"
1361
+ ].includes(request.status),
1362
+ "INVALID_CONTRACT",
1363
+ "Spend request has an unsupported status"
1364
+ );
1365
+ economyAssert(
1366
+ parseTokenSubunits(request.amount) > 0n,
1367
+ "INVALID_AMOUNT",
1368
+ "Spend-request amount must be positive"
1369
+ );
1370
+ const requestedAt = parseIsoTimestamp(request.requestedAt);
1371
+ const expiresAt = parseIsoTimestamp(request.expiresAt);
1372
+ economyAssert(
1373
+ expiresAt > requestedAt,
1374
+ "INVALID_TIME_WINDOW",
1375
+ "Spend-request expiry must follow creation"
1376
+ );
1377
+ const decisionRequired = ["approved", "rejected", "executed"].includes(
1378
+ request.status
1379
+ );
1380
+ economyAssert(
1381
+ !decisionRequired || request.decidedByAccountId !== void 0 && request.decidedAt !== void 0,
1382
+ "INVALID_CONTRACT",
1383
+ "Decided spend requests require a guardian actor and timestamp"
1384
+ );
1385
+ if (request.decidedByAccountId !== void 0) {
1386
+ assertEconomyIdentifier(request.decidedByAccountId, "decidedByAccountId");
1387
+ }
1388
+ if (request.decidedAt !== void 0) {
1389
+ const decidedAt = parseIsoTimestamp(request.decidedAt);
1390
+ economyAssert(
1391
+ decidedAt >= requestedAt && decidedAt <= expiresAt,
1392
+ "INVALID_TIME_WINDOW",
1393
+ "Spend-request decision must occur before expiry"
1394
+ );
1395
+ }
1396
+ if (request.approvalNonceHash !== void 0) {
1397
+ economyAssert(
1398
+ /^sha256:[a-f0-9]{64}$/u.test(request.approvalNonceHash),
1399
+ "INVALID_CONTRACT",
1400
+ "Approval nonces must be stored as SHA-256 hashes"
1401
+ );
1402
+ }
1403
+ if (request.status === "approved" || request.status === "executed") {
1404
+ economyAssert(
1405
+ request.approvalNonceHash !== void 0,
1406
+ "INVALID_CONTRACT",
1407
+ "Approved spend requests require a one-time approval nonce hash"
1408
+ );
1409
+ }
1410
+ if (request.resultingTransactionId !== void 0) {
1411
+ assertEconomyIdentifier(
1412
+ request.resultingTransactionId,
1413
+ "resultingTransactionId"
1414
+ );
1415
+ economyAssert(
1416
+ request.status === "executed",
1417
+ "INVALID_CONTRACT",
1418
+ "Only executed spend requests may reference a ledger transaction"
1419
+ );
1420
+ }
1421
+ }
1422
+
1423
+ // src/wallets.ts
1424
+ function assertWallet(wallet) {
1425
+ economyAssert(
1426
+ wallet.schemaVersion === ECONOMY_CONTRACT_VERSION,
1427
+ "INVALID_CONTRACT",
1428
+ "Unsupported wallet contract version"
1429
+ );
1430
+ assertEconomyIdentifier(wallet.walletId, "walletId");
1431
+ assertEconomyIdentifier(wallet.accountId, "accountId");
1432
+ economyAssert(
1433
+ [
1434
+ "household-treasury",
1435
+ "personal",
1436
+ "gameplay-allocation",
1437
+ "hold",
1438
+ "purchase-clearing",
1439
+ "provider-clearing",
1440
+ "system"
1441
+ ].includes(wallet.kind),
1442
+ "INVALID_CONTRACT",
1443
+ "Wallet has an unsupported kind"
1444
+ );
1445
+ economyAssert(
1446
+ ["account", "household", "system"].includes(wallet.ownerType),
1447
+ "INVALID_CONTRACT",
1448
+ "Wallet has an unsupported owner type"
1449
+ );
1450
+ economyAssert(
1451
+ ["active", "restricted", "closed"].includes(wallet.status),
1452
+ "INVALID_CONTRACT",
1453
+ "Wallet has an unsupported status"
1454
+ );
1455
+ if (wallet.ownerType === "system") {
1456
+ economyAssert(
1457
+ wallet.ownerId === "system",
1458
+ "INVALID_CONTRACT",
1459
+ "System wallets must be owned by the system"
1460
+ );
1461
+ } else {
1462
+ assertEconomyIdentifier(wallet.ownerId, "ownerId");
1463
+ }
1464
+ if (wallet.householdId !== void 0) {
1465
+ assertEconomyIdentifier(wallet.householdId, "householdId");
1466
+ }
1467
+ if (wallet.ownerType === "household") {
1468
+ economyAssert(
1469
+ wallet.householdId === wallet.ownerId,
1470
+ "INVALID_CONTRACT",
1471
+ "Household wallets must identify the same household owner"
1472
+ );
1473
+ }
1474
+ if (wallet.kind === "household-treasury") {
1475
+ economyAssert(
1476
+ wallet.ownerType === "household" && wallet.householdId !== void 0,
1477
+ "INVALID_CONTRACT",
1478
+ "Household treasuries must be owned by their household"
1479
+ );
1480
+ }
1481
+ if (wallet.kind === "personal" || wallet.kind === "gameplay-allocation") {
1482
+ economyAssert(
1483
+ wallet.ownerType === "account",
1484
+ "INVALID_CONTRACT",
1485
+ "Personal and gameplay-allocation wallets must be account-owned"
1486
+ );
1487
+ }
1488
+ if (wallet.kind === "gameplay-allocation") {
1489
+ economyAssert(
1490
+ wallet.householdId !== void 0,
1491
+ "INVALID_CONTRACT",
1492
+ "Gameplay-allocation wallets must retain household provenance"
1493
+ );
1494
+ }
1495
+ if (wallet.kind === "purchase-clearing" || wallet.kind === "provider-clearing" || wallet.kind === "system") {
1496
+ economyAssert(
1497
+ wallet.ownerType === "system" && wallet.householdId === void 0,
1498
+ "INVALID_CONTRACT",
1499
+ "Clearing and system wallets must be system-owned"
1500
+ );
1501
+ }
1502
+ economyAssert(
1503
+ Number.isSafeInteger(wallet.version) && wallet.version >= 1,
1504
+ "INVALID_CONTRACT",
1505
+ "Wallet version must be a positive safe integer"
1506
+ );
1507
+ const createdAt = parseIsoTimestamp(wallet.createdAt);
1508
+ if (wallet.closedAt !== void 0) {
1509
+ economyAssert(
1510
+ parseIsoTimestamp(wallet.closedAt) >= createdAt && wallet.status === "closed",
1511
+ "INVALID_TIME_WINDOW",
1512
+ "Only closed wallets may have a valid closure time"
1513
+ );
1514
+ } else {
1515
+ economyAssert(
1516
+ wallet.status !== "closed",
1517
+ "INVALID_CONTRACT",
1518
+ "Closed wallets require a closure time"
1519
+ );
1520
+ }
1521
+ }
1522
+ function assertWalletBalanceSummary(summary) {
1523
+ economyAssert(
1524
+ summary.schemaVersion === ECONOMY_CONTRACT_VERSION,
1525
+ "INVALID_CONTRACT",
1526
+ "Unsupported wallet summary contract version"
1527
+ );
1528
+ assertEconomyIdentifier(summary.walletId, "walletId");
1529
+ for (const amount of [
1530
+ summary.available,
1531
+ summary.reserved,
1532
+ summary.held,
1533
+ summary.rewardProgress
1534
+ ]) {
1535
+ economyAssert(
1536
+ parseTokenSubunits(amount) >= 0n,
1537
+ "INVALID_AMOUNT",
1538
+ "Wallet summary categories cannot be negative"
1539
+ );
1540
+ }
1541
+ economyAssert(
1542
+ Number.isSafeInteger(summary.version) && summary.version >= 1,
1543
+ "INVALID_CONTRACT",
1544
+ "Wallet summary version must be a positive safe integer"
1545
+ );
1546
+ parseIsoTimestamp(summary.asOf);
1547
+ }
1548
+ function assertWalletLifetimeTotals(totals) {
1549
+ economyAssert(
1550
+ totals.schemaVersion === ECONOMY_CONTRACT_VERSION,
1551
+ "INVALID_CONTRACT",
1552
+ "Unsupported wallet lifetime-total contract version"
1553
+ );
1554
+ for (const amount of [
1555
+ totals.bought,
1556
+ totals.earned,
1557
+ totals.allocated,
1558
+ totals.reclaimed,
1559
+ totals.spent,
1560
+ totals.reversed
1561
+ ]) {
1562
+ economyAssert(
1563
+ parseTokenSubunits(amount) >= 0n,
1564
+ "INVALID_AMOUNT",
1565
+ "Wallet lifetime totals cannot be negative"
1566
+ );
1567
+ }
1568
+ }
1569
+ export {
1570
+ BASELINE_GBP_CATALOG_VERSION,
1571
+ BASELINE_GBP_REFERENCE_RATE,
1572
+ BASELINE_GBP_TOKEN_PACKS,
1573
+ BASELINE_MONTHLY_SUBSCRIPTION_PLAN,
1574
+ BASELINE_PURCHASE_LIMIT_POLICY,
1575
+ ECONOMY_CONTRACT_VERSION,
1576
+ EconomyError,
1577
+ PRE_UTILITY_BACKER_COHORT,
1578
+ TOKEN_SUBUNITS_MAX,
1579
+ TOKEN_SUBUNITS_MIN,
1580
+ TOKEN_SUBUNITS_PER_TOKEN,
1581
+ applyTransactionToProjection,
1582
+ assertActivityEntry,
1583
+ assertBalancedTransaction,
1584
+ assertDistinctAdjustmentApproval,
1585
+ assertEconomyIdentifier,
1586
+ assertFlatTokenCatalog,
1587
+ assertFxSnapshot,
1588
+ assertGameplayAllocation,
1589
+ assertNonNegativeAccounts,
1590
+ assertOpenPurchaseIntent,
1591
+ assertPurchaseIntentBinding,
1592
+ assertPurchaseLimitPolicy,
1593
+ assertReversalAvailable,
1594
+ assertRewardConversion,
1595
+ assertRewardRate,
1596
+ assertSourceLot,
1597
+ assertSpendRequest,
1598
+ assertTokenPack,
1599
+ assertTokenReferenceRate,
1600
+ assertTokenSubscriptionPlan,
1601
+ assertWallet,
1602
+ assertWalletBalanceSummary,
1603
+ assertWalletLifetimeTotals,
1604
+ availableSourceLotAmount,
1605
+ boostGameplayAllocation,
1606
+ canUseSourceLot,
1607
+ canonicalTransactionPayload,
1608
+ convertRewardPayout,
1609
+ createGameplayAllocation,
1610
+ createProjectionSnapshot,
1611
+ createReversalTransaction,
1612
+ economyAssert,
1613
+ evaluateEarlyBacker,
1614
+ isWholeTokenAmount,
1615
+ mapBitLabsConversionState,
1616
+ parseIsoTimestamp,
1617
+ parseTokenSubunits,
1618
+ rebuildBalanceProjection,
1619
+ reclaimGameplayAllocation,
1620
+ selectSourceLots,
1621
+ serializeTokenSubunits,
1622
+ sortStringRecord,
1623
+ subunitsToWholeTokens,
1624
+ sumTokenSubunits,
1625
+ toEconomyErrorEnvelope,
1626
+ wholeTokensToSubunits
1627
+ };
1628
+ //# sourceMappingURL=index.js.map