@plasius/economy 0.1.0 → 0.3.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.cjs CHANGED
@@ -25,20 +25,31 @@ __export(index_exports, {
25
25
  BASELINE_GBP_TOKEN_PACKS: () => BASELINE_GBP_TOKEN_PACKS,
26
26
  BASELINE_MONTHLY_SUBSCRIPTION_PLAN: () => BASELINE_MONTHLY_SUBSCRIPTION_PLAN,
27
27
  BASELINE_PURCHASE_LIMIT_POLICY: () => BASELINE_PURCHASE_LIMIT_POLICY,
28
+ EARLY_BACKER_SETTLEMENT_POLICY_V2: () => EARLY_BACKER_SETTLEMENT_POLICY_V2,
28
29
  ECONOMY_CONTRACT_VERSION: () => ECONOMY_CONTRACT_VERSION,
29
30
  EconomyError: () => EconomyError,
30
31
  PRE_UTILITY_BACKER_COHORT: () => PRE_UTILITY_BACKER_COHORT,
31
32
  TOKEN_SUBUNITS_MAX: () => TOKEN_SUBUNITS_MAX,
32
33
  TOKEN_SUBUNITS_MIN: () => TOKEN_SUBUNITS_MIN,
33
34
  TOKEN_SUBUNITS_PER_TOKEN: () => TOKEN_SUBUNITS_PER_TOKEN,
35
+ advanceJournalChainHead: () => advanceJournalChainHead,
36
+ applySourceLotMovement: () => applySourceLotMovement,
34
37
  applyTransactionToProjection: () => applyTransactionToProjection,
38
+ applyWalletBalanceDelta: () => applyWalletBalanceDelta,
39
+ applyWalletLifetimeDeltas: () => applyWalletLifetimeDeltas,
35
40
  assertActivityEntry: () => assertActivityEntry,
36
41
  assertBalancedTransaction: () => assertBalancedTransaction,
42
+ assertDeterministicWalletBalanceSummary: () => assertDeterministicWalletBalanceSummary,
37
43
  assertDistinctAdjustmentApproval: () => assertDistinctAdjustmentApproval,
44
+ assertEconomicJournalTransaction: () => assertEconomicJournalTransaction,
45
+ assertEconomyCommandEnvelope: () => assertEconomyCommandEnvelope,
46
+ assertEconomyCommandWorkflowEvent: () => assertEconomyCommandWorkflowEvent,
47
+ assertEconomyIdempotencyScope: () => assertEconomyIdempotencyScope,
38
48
  assertEconomyIdentifier: () => assertEconomyIdentifier,
39
49
  assertFlatTokenCatalog: () => assertFlatTokenCatalog,
40
50
  assertFxSnapshot: () => assertFxSnapshot,
41
51
  assertGameplayAllocation: () => assertGameplayAllocation,
52
+ assertJournalChainHead: () => assertJournalChainHead,
42
53
  assertNonNegativeAccounts: () => assertNonNegativeAccounts,
43
54
  assertOpenPurchaseIntent: () => assertOpenPurchaseIntent,
44
55
  assertPurchaseIntentBinding: () => assertPurchaseIntentBinding,
@@ -46,24 +57,46 @@ __export(index_exports, {
46
57
  assertReversalAvailable: () => assertReversalAvailable,
47
58
  assertRewardConversion: () => assertRewardConversion,
48
59
  assertRewardRate: () => assertRewardRate,
60
+ assertScopedPersistedIdempotencyResult: () => assertScopedPersistedIdempotencyResult,
49
61
  assertSourceLot: () => assertSourceLot,
62
+ assertSourceLotMovement: () => assertSourceLotMovement,
50
63
  assertSpendRequest: () => assertSpendRequest,
51
64
  assertTokenPack: () => assertTokenPack,
52
65
  assertTokenReferenceRate: () => assertTokenReferenceRate,
53
66
  assertTokenSubscriptionPlan: () => assertTokenSubscriptionPlan,
67
+ assertVersionedSourceLot: () => assertVersionedSourceLot,
54
68
  assertWallet: () => assertWallet,
69
+ assertWalletActivityEntry: () => assertWalletActivityEntry,
70
+ assertWalletActivityPage: () => assertWalletActivityPage,
71
+ assertWalletActivityPageForPortfolio: () => assertWalletActivityPageForPortfolio,
72
+ assertWalletActivityPageRequest: () => assertWalletActivityPageRequest,
73
+ assertWalletBalanceDelta: () => assertWalletBalanceDelta,
74
+ assertWalletBalanceDeltaBatch: () => assertWalletBalanceDeltaBatch,
75
+ assertWalletBalanceProjection: () => assertWalletBalanceProjection,
55
76
  assertWalletBalanceSummary: () => assertWalletBalanceSummary,
77
+ assertWalletLifetimeSnapshot: () => assertWalletLifetimeSnapshot,
56
78
  assertWalletLifetimeTotals: () => assertWalletLifetimeTotals,
79
+ assertWalletOwnerReference: () => assertWalletOwnerReference,
80
+ assertWalletPortfolioLifetime: () => assertWalletPortfolioLifetime,
81
+ assertWalletPortfolioReadScope: () => assertWalletPortfolioReadScope,
82
+ assertWalletPortfolioSummary: () => assertWalletPortfolioSummary,
57
83
  availableSourceLotAmount: () => availableSourceLotAmount,
58
84
  boostGameplayAllocation: () => boostGameplayAllocation,
59
85
  canUseSourceLot: () => canUseSourceLot,
60
86
  canonicalTransactionPayload: () => canonicalTransactionPayload,
61
87
  convertRewardPayout: () => convertRewardPayout,
62
88
  createGameplayAllocation: () => createGameplayAllocation,
89
+ createInitialSourceLotSnapshot: () => createInitialSourceLotSnapshot,
63
90
  createProjectionSnapshot: () => createProjectionSnapshot,
64
91
  createReversalTransaction: () => createReversalTransaction,
92
+ createWalletBalanceSummary: () => createWalletBalanceSummary,
93
+ createWalletPortfolioLifetime: () => createWalletPortfolioLifetime,
94
+ createWalletPortfolioSummary: () => createWalletPortfolioSummary,
95
+ deriveWalletBalanceDeltas: () => deriveWalletBalanceDeltas,
96
+ deriveWalletLifetimeDeltas: () => deriveWalletLifetimeDeltas,
65
97
  economyAssert: () => economyAssert,
66
98
  evaluateEarlyBacker: () => evaluateEarlyBacker,
99
+ evaluateEarlyBackerBySettlementV2: () => evaluateEarlyBackerBySettlementV2,
67
100
  isWholeTokenAmount: () => isWholeTokenAmount,
68
101
  mapBitLabsConversionState: () => mapBitLabsConversionState,
69
102
  parseIsoTimestamp: () => parseIsoTimestamp,
@@ -119,7 +152,7 @@ function assertInRange(amount) {
119
152
  }
120
153
  function parseTokenSubunits(value) {
121
154
  economyAssert(
122
- CANONICAL_INTEGER.test(value),
155
+ typeof value === "string" && CANONICAL_INTEGER.test(value),
123
156
  "INVALID_AMOUNT",
124
157
  "TokenSubunit amount must be a canonical base-10 integer string"
125
158
  );
@@ -168,12 +201,17 @@ var ECONOMY_CONTRACT_VERSION = "1";
168
201
  var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
169
202
  function assertEconomyIdentifier(value, label = "identifier") {
170
203
  economyAssert(
171
- IDENTIFIER.test(value),
204
+ typeof value === "string" && IDENTIFIER.test(value),
172
205
  "INVALID_CONTRACT",
173
206
  `${label} must be a non-empty opaque identifier`
174
207
  );
175
208
  }
176
209
  function parseIsoTimestamp(value) {
210
+ economyAssert(
211
+ typeof value === "string",
212
+ "INVALID_CONTRACT",
213
+ "Timestamp must be an ISO-8601 UTC value"
214
+ );
177
215
  const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{3}))?Z$/u.exec(
178
216
  value
179
217
  );
@@ -798,6 +836,7 @@ function reclaimGameplayAllocation(allocation, input) {
798
836
 
799
837
  // src/early-backers.ts
800
838
  var PRE_UTILITY_BACKER_COHORT = "pre_utility_backer_v1";
839
+ var EARLY_BACKER_SETTLEMENT_POLICY_V2 = "settlement-window-v2";
801
840
  function isWithinWindow(timestamp, launch, cutoff) {
802
841
  const value = parseIsoTimestamp(timestamp);
803
842
  return value >= launch && (cutoff === void 0 || value < cutoff);
@@ -869,6 +908,71 @@ function evaluateEarlyBacker(payerAccountId, receivingHouseholdId, paidLots, win
869
908
  evaluatedAt
870
909
  };
871
910
  }
911
+ function evaluateEarlyBackerBySettlementV2(payerAccountId, receivingHouseholdId, paidLots, window, evaluatedAt) {
912
+ assertEconomyIdentifier(payerAccountId, "payerAccountId");
913
+ assertEconomyIdentifier(receivingHouseholdId, "receivingHouseholdId");
914
+ const launch = parseIsoTimestamp(window.publicTokensLaunchAt);
915
+ const cutoff = window.firstPublicSpendLiveAt === void 0 ? void 0 : parseIsoTimestamp(window.firstPublicSpendLiveAt);
916
+ economyAssert(
917
+ cutoff === void 0 || cutoff > launch,
918
+ "INVALID_TIME_WINDOW",
919
+ "First public spend must occur after public Token launch"
920
+ );
921
+ const evaluationTime = parseIsoTimestamp(evaluatedAt);
922
+ let retained = 0n;
923
+ const contributingLotIds = [];
924
+ const seenLotIds = /* @__PURE__ */ new Set();
925
+ for (const lot of paidLots) {
926
+ economyAssert(
927
+ lot.schemaVersion === ECONOMY_CONTRACT_VERSION,
928
+ "INVALID_CONTRACT",
929
+ "Unsupported paid-lot retention contract version"
930
+ );
931
+ assertEconomyIdentifier(lot.lotId, "lotId");
932
+ economyAssert(
933
+ !seenLotIds.has(lot.lotId),
934
+ "DUPLICATE_IDENTIFIER",
935
+ "Paid-lot retention input must not repeat a source lot"
936
+ );
937
+ seenLotIds.add(lot.lotId);
938
+ assertEconomyIdentifier(lot.payerAccountId, "payerAccountId");
939
+ assertEconomyIdentifier(lot.receivingHouseholdId, "receivingHouseholdId");
940
+ assertEconomyIdentifier(lot.purchaseId, "purchaseId");
941
+ assertEconomyIdentifier(lot.catalogVersion, "catalogVersion");
942
+ const purchasedAt = parseIsoTimestamp(lot.purchasedAt);
943
+ const settledAt = parseIsoTimestamp(lot.settledAt);
944
+ const creditedAt = parseIsoTimestamp(lot.creditedAt);
945
+ economyAssert(
946
+ purchasedAt <= settledAt && settledAt <= creditedAt,
947
+ "INVALID_TIME_WINDOW",
948
+ "Paid-lot timestamps must follow purchase, settlement, then credit order"
949
+ );
950
+ const amount = parseTokenSubunits(lot.retainedAmount);
951
+ economyAssert(
952
+ amount >= 0n,
953
+ "INVALID_AMOUNT",
954
+ "Retained paid-lot basis cannot be negative"
955
+ );
956
+ if (lot.payerAccountId === payerAccountId && lot.receivingHouseholdId === receivingHouseholdId && settledAt >= launch && (cutoff === void 0 || settledAt < cutoff) && settledAt <= evaluationTime && amount > 0n) {
957
+ retained += amount;
958
+ contributingLotIds.push(lot.lotId);
959
+ }
960
+ }
961
+ return {
962
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
963
+ policyVersion: EARLY_BACKER_SETTLEMENT_POLICY_V2,
964
+ qualificationEvent: "settled-at",
965
+ cohortKey: PRE_UTILITY_BACKER_COHORT,
966
+ status: retained > 0n ? "provisional" : "not-qualified",
967
+ payerAccountId,
968
+ receivingHouseholdId,
969
+ netRetainedAmount: serializeTokenSubunits(retained),
970
+ contributingLotIds: contributingLotIds.sort(
971
+ (left, right) => left.localeCompare(right)
972
+ ),
973
+ evaluatedAt
974
+ };
975
+ }
872
976
 
873
977
  // src/ledger.ts
874
978
  var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
@@ -895,6 +999,15 @@ var ACTIVITY_STATUSES = /* @__PURE__ */ new Set([
895
999
  "reversed",
896
1000
  "failed"
897
1001
  ]);
1002
+ var ACTIVITY_SOURCES = /* @__PURE__ */ new Set([
1003
+ "shopify",
1004
+ "ayet",
1005
+ "bitlabs",
1006
+ "subscription",
1007
+ "event",
1008
+ "competition",
1009
+ "adjustment"
1010
+ ]);
898
1011
  function assertCanonicalHash(value, label) {
899
1012
  economyAssert(
900
1013
  /^sha256:[a-f0-9]{64}$/u.test(value),
@@ -916,9 +1029,9 @@ function assertActivityEntry(entry) {
916
1029
  );
917
1030
  assertEconomyIdentifier(entry.transactionId, "transactionId");
918
1031
  economyAssert(
919
- ACTIVITY_TYPES.has(entry.activityType) && ACTIVITY_STATUSES.has(entry.status),
1032
+ ACTIVITY_TYPES.has(entry.activityType) && ACTIVITY_STATUSES.has(entry.status) && ACTIVITY_SOURCES.has(entry.source),
920
1033
  "INVALID_CONTRACT",
921
- "Activity type or status is unsupported"
1034
+ "Activity type, status, or source is unsupported"
922
1035
  );
923
1036
  parseIsoTimestamp(entry.occurredAt);
924
1037
  economyAssert(
@@ -934,13 +1047,13 @@ function assertActivityEntry(entry) {
934
1047
  }
935
1048
  if (entry.maskedReference !== void 0) {
936
1049
  economyAssert(
937
- entry.maskedReference.length > 0 && entry.maskedReference.length <= 128 && !hasControlCharacters(entry.maskedReference),
1050
+ typeof entry.maskedReference === "string" && entry.maskedReference.length > 0 && entry.maskedReference.length <= 128 && !hasControlCharacters(entry.maskedReference),
938
1051
  "INVALID_CONTRACT",
939
1052
  "Masked activity references must be bounded and safe for display"
940
1053
  );
941
1054
  }
942
1055
  economyAssert(
943
- entry.sourceLabel.trim().length > 0 && entry.sourceLabel.length <= 128 && !hasControlCharacters(entry.sourceLabel),
1056
+ typeof entry.sourceLabel === "string" && entry.sourceLabel.trim().length > 0 && entry.sourceLabel.length <= 128 && !hasControlCharacters(entry.sourceLabel),
944
1057
  "INVALID_CONTRACT",
945
1058
  "Activity source labels must be bounded and safe for display"
946
1059
  );
@@ -1039,6 +1152,14 @@ function assertBalancedTransaction(transaction) {
1039
1152
  "Ledger postings must sum to zero TokenSubunits"
1040
1153
  );
1041
1154
  }
1155
+ function assertEconomicJournalTransaction(transaction) {
1156
+ assertBalancedTransaction(transaction);
1157
+ economyAssert(
1158
+ transaction.status === "held" || transaction.status === "settled",
1159
+ "INVALID_CONTRACT",
1160
+ "Pending, failed, and derived reversed activity cannot be journal transactions"
1161
+ );
1162
+ }
1042
1163
  function canonicalTransactionPayload(transaction) {
1043
1164
  assertBalancedTransaction(transaction);
1044
1165
  const postings = [...transaction.postings].sort((left, right) => left.postingId.localeCompare(right.postingId)).map((posting) => ({
@@ -1118,6 +1239,71 @@ function assertReversalAvailable(originalTransactionId, existingTransactions) {
1118
1239
  );
1119
1240
  }
1120
1241
 
1242
+ // src/integrity.ts
1243
+ function assertCanonicalHash2(value, label) {
1244
+ economyAssert(
1245
+ typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value),
1246
+ "INVALID_CONTRACT",
1247
+ `${label} must be a canonical SHA-256 reference`
1248
+ );
1249
+ }
1250
+ function assertJournalChainHead(head) {
1251
+ economyAssert(
1252
+ head.schemaVersion === ECONOMY_CONTRACT_VERSION,
1253
+ "INVALID_CONTRACT",
1254
+ "Unsupported journal-chain-head contract version"
1255
+ );
1256
+ assertEconomyIdentifier(head.chainId, "chainId");
1257
+ economyAssert(
1258
+ Number.isSafeInteger(head.version) && head.version >= 0,
1259
+ "INVALID_CONTRACT",
1260
+ "Journal-chain-head version must be a non-negative safe integer"
1261
+ );
1262
+ parseIsoTimestamp(head.updatedAt);
1263
+ const hasTransaction = head.lastTransactionId !== void 0;
1264
+ const hasHash = head.canonicalHash !== void 0;
1265
+ economyAssert(
1266
+ hasTransaction === hasHash && head.version > 0 === hasTransaction,
1267
+ "INVALID_CONTRACT",
1268
+ "Journal-chain head identity, hash, and version must describe the same state"
1269
+ );
1270
+ if (head.lastTransactionId !== void 0) {
1271
+ assertEconomyIdentifier(head.lastTransactionId, "lastTransactionId");
1272
+ }
1273
+ if (head.canonicalHash !== void 0) {
1274
+ assertCanonicalHash2(head.canonicalHash, "Journal-chain-head hash");
1275
+ }
1276
+ }
1277
+ function advanceJournalChainHead(head, transaction) {
1278
+ assertJournalChainHead(head);
1279
+ assertEconomicJournalTransaction(transaction);
1280
+ assertCanonicalHash2(transaction.canonicalHash, "Transaction hash");
1281
+ economyAssert(
1282
+ transaction.previousCanonicalHash === head.canonicalHash,
1283
+ "INVALID_CONTRACT",
1284
+ "Transaction previous hash must match the locked journal-chain head"
1285
+ );
1286
+ economyAssert(
1287
+ transaction.canonicalHash !== head.canonicalHash,
1288
+ "DUPLICATE_TRANSACTION",
1289
+ "Transaction hash must advance the journal chain"
1290
+ );
1291
+ const recordedAt = parseIsoTimestamp(transaction.recordedAt);
1292
+ economyAssert(
1293
+ recordedAt >= parseIsoTimestamp(head.updatedAt),
1294
+ "INVALID_TIME_WINDOW",
1295
+ "Journal-chain update cannot precede its locked head"
1296
+ );
1297
+ return {
1298
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1299
+ chainId: head.chainId,
1300
+ version: head.version + 1,
1301
+ lastTransactionId: transaction.transactionId,
1302
+ canonicalHash: transaction.canonicalHash,
1303
+ updatedAt: transaction.recordedAt
1304
+ };
1305
+ }
1306
+
1121
1307
  // src/lots.ts
1122
1308
  function assertSourceLot(lot) {
1123
1309
  economyAssert(
@@ -1214,6 +1400,199 @@ function assertSourceLot(lot) {
1214
1400
  "Remaining and reversed source-lot amounts exceed the original"
1215
1401
  );
1216
1402
  }
1403
+ function assertVersionedSourceLot(snapshot) {
1404
+ economyAssert(
1405
+ snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
1406
+ "INVALID_CONTRACT",
1407
+ "Unsupported versioned source-lot contract version"
1408
+ );
1409
+ assertSourceLot(snapshot.lot);
1410
+ economyAssert(
1411
+ Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
1412
+ "INVALID_CONTRACT",
1413
+ "Source-lot version must be a positive safe integer"
1414
+ );
1415
+ economyAssert(
1416
+ parseIsoTimestamp(snapshot.updatedAt) >= parseIsoTimestamp(snapshot.lot.creditedAt),
1417
+ "INVALID_TIME_WINDOW",
1418
+ "Source-lot update time cannot precede its credit"
1419
+ );
1420
+ }
1421
+ function createInitialSourceLotSnapshot(lot) {
1422
+ assertSourceLot(lot);
1423
+ economyAssert(
1424
+ lot.refundState === "none" && lot.originalAmount === lot.remainingAmount && parseTokenSubunits(lot.heldAmount) === 0n && parseTokenSubunits(lot.reversedAmount) === 0n,
1425
+ "INVALID_CONTRACT",
1426
+ "A new source lot must start fully remaining, clear, and unreversed"
1427
+ );
1428
+ return {
1429
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1430
+ lot,
1431
+ version: 1,
1432
+ updatedAt: lot.creditedAt
1433
+ };
1434
+ }
1435
+ function assertRefundState(value) {
1436
+ economyAssert(
1437
+ [
1438
+ "none",
1439
+ "partial",
1440
+ "refunded",
1441
+ "disputed",
1442
+ "chargeback-lost"
1443
+ ].includes(value),
1444
+ "INVALID_CONTRACT",
1445
+ "Source-lot movement has an unsupported refund state"
1446
+ );
1447
+ }
1448
+ function assertSourceLotMovement(movement) {
1449
+ economyAssert(
1450
+ movement.schemaVersion === ECONOMY_CONTRACT_VERSION,
1451
+ "INVALID_CONTRACT",
1452
+ "Unsupported source-lot-movement contract version"
1453
+ );
1454
+ assertEconomyIdentifier(movement.movementId, "movementId");
1455
+ assertEconomyIdentifier(movement.transactionId, "transactionId");
1456
+ assertEconomyIdentifier(movement.lotId, "lotId");
1457
+ economyAssert(
1458
+ [
1459
+ "allocate",
1460
+ "boost",
1461
+ "reclaim",
1462
+ "spend",
1463
+ "hold",
1464
+ "release-hold",
1465
+ "refund",
1466
+ "chargeback",
1467
+ "reversal"
1468
+ ].includes(movement.movementType),
1469
+ "INVALID_CONTRACT",
1470
+ "Source-lot movement has an unsupported type"
1471
+ );
1472
+ const remaining = parseTokenSubunits(movement.remainingDelta);
1473
+ const held = parseTokenSubunits(movement.heldDelta);
1474
+ const reversed = parseTokenSubunits(movement.reversedDelta);
1475
+ economyAssert(
1476
+ remaining !== 0n || held !== 0n || reversed !== 0n,
1477
+ "INVALID_AMOUNT",
1478
+ "Source-lot movement must change at least one amount"
1479
+ );
1480
+ economyAssert(
1481
+ Number.isSafeInteger(movement.expectedVersion) && movement.expectedVersion >= 1 && movement.resultingVersion === movement.expectedVersion + 1,
1482
+ "INVALID_CONTRACT",
1483
+ "Source-lot movement must advance its optimistic version exactly once"
1484
+ );
1485
+ assertRefundState(movement.expectedRefundState);
1486
+ assertRefundState(movement.resultingRefundState);
1487
+ parseIsoTimestamp(movement.occurredAt);
1488
+ if (movement.movementType === "allocate" || movement.movementType === "boost" || movement.movementType === "spend") {
1489
+ economyAssert(
1490
+ remaining < 0n && held === 0n && reversed === 0n && movement.resultingRefundState === movement.expectedRefundState,
1491
+ "INVALID_CONTRACT",
1492
+ "Allocation, boost, and spend movements only consume remaining lot value"
1493
+ );
1494
+ } else if (movement.movementType === "reclaim") {
1495
+ economyAssert(
1496
+ remaining > 0n && held === 0n && reversed === 0n && movement.resultingRefundState === movement.expectedRefundState,
1497
+ "INVALID_CONTRACT",
1498
+ "Reclaim movements only restore remaining lot value"
1499
+ );
1500
+ } else if (movement.movementType === "hold") {
1501
+ economyAssert(
1502
+ remaining === 0n && held > 0n && reversed === 0n && movement.resultingRefundState === "disputed",
1503
+ "INVALID_CONTRACT",
1504
+ "Hold movements must enter disputed state and increase held value"
1505
+ );
1506
+ } else if (movement.movementType === "release-hold") {
1507
+ economyAssert(
1508
+ remaining === 0n && held < 0n && reversed === 0n && movement.expectedRefundState === "disputed" && movement.resultingRefundState !== "disputed" && movement.resultingRefundState !== "refunded" && movement.resultingRefundState !== "chargeback-lost",
1509
+ "INVALID_CONTRACT",
1510
+ "Released holds must leave disputed state without reversing value"
1511
+ );
1512
+ } else {
1513
+ economyAssert(
1514
+ remaining < 0n && reversed === -remaining && held <= 0n && (movement.resultingRefundState === "partial" || movement.resultingRefundState === "refunded" || movement.resultingRefundState === "chargeback-lost"),
1515
+ "INVALID_CONTRACT",
1516
+ "Refund, chargeback, and reversal movements must compensate remaining value exactly"
1517
+ );
1518
+ if (movement.movementType === "chargeback") {
1519
+ economyAssert(
1520
+ movement.resultingRefundState === "chargeback-lost",
1521
+ "INVALID_CONTRACT",
1522
+ "Chargeback movements must enter chargeback-lost state"
1523
+ );
1524
+ } else {
1525
+ economyAssert(
1526
+ movement.resultingRefundState !== "chargeback-lost",
1527
+ "INVALID_CONTRACT",
1528
+ "Only a chargeback movement may enter chargeback-lost state"
1529
+ );
1530
+ }
1531
+ }
1532
+ }
1533
+ function applySourceLotMovement(snapshot, movement) {
1534
+ assertVersionedSourceLot(snapshot);
1535
+ assertSourceLotMovement(movement);
1536
+ economyAssert(
1537
+ movement.lotId === snapshot.lot.lotId && movement.expectedVersion === snapshot.version && movement.expectedRefundState === snapshot.lot.refundState,
1538
+ "INVALID_CONTRACT",
1539
+ "Source-lot movement does not match the locked lot version and refund state"
1540
+ );
1541
+ economyAssert(
1542
+ parseIsoTimestamp(movement.occurredAt) >= parseIsoTimestamp(snapshot.updatedAt),
1543
+ "INVALID_TIME_WINDOW",
1544
+ "Source-lot movement cannot precede its current projection"
1545
+ );
1546
+ const lot = {
1547
+ ...snapshot.lot,
1548
+ remainingAmount: serializeTokenSubunits(
1549
+ parseTokenSubunits(snapshot.lot.remainingAmount) + parseTokenSubunits(movement.remainingDelta)
1550
+ ),
1551
+ heldAmount: serializeTokenSubunits(
1552
+ parseTokenSubunits(snapshot.lot.heldAmount) + parseTokenSubunits(movement.heldDelta)
1553
+ ),
1554
+ reversedAmount: serializeTokenSubunits(
1555
+ parseTokenSubunits(snapshot.lot.reversedAmount) + parseTokenSubunits(movement.reversedDelta)
1556
+ ),
1557
+ refundState: movement.resultingRefundState
1558
+ };
1559
+ assertSourceLot(lot);
1560
+ const remaining = parseTokenSubunits(lot.remainingAmount);
1561
+ const held = parseTokenSubunits(lot.heldAmount);
1562
+ const reversed = parseTokenSubunits(lot.reversedAmount);
1563
+ const original = parseTokenSubunits(lot.originalAmount);
1564
+ if (lot.refundState === "none") {
1565
+ economyAssert(
1566
+ reversed === 0n && held === 0n,
1567
+ "INVALID_CONTRACT",
1568
+ "A clear lot cannot retain held or reversed value"
1569
+ );
1570
+ } else if (lot.refundState === "partial") {
1571
+ economyAssert(
1572
+ reversed > 0n && remaining > 0n && held === 0n,
1573
+ "INVALID_CONTRACT",
1574
+ "A partial refund requires retained/reversed value and no active hold"
1575
+ );
1576
+ } else if (lot.refundState === "disputed") {
1577
+ economyAssert(
1578
+ held > 0n,
1579
+ "INVALID_CONTRACT",
1580
+ "A disputed lot must hold affected value"
1581
+ );
1582
+ } else {
1583
+ economyAssert(
1584
+ remaining === 0n && held === 0n && reversed === original,
1585
+ "INVALID_CONTRACT",
1586
+ "Final refund and lost-chargeback states must reverse the full lot"
1587
+ );
1588
+ }
1589
+ return {
1590
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1591
+ lot,
1592
+ version: movement.resultingVersion,
1593
+ updatedAt: movement.occurredAt
1594
+ };
1595
+ }
1217
1596
  function availableSourceLotAmount(lot) {
1218
1597
  assertSourceLot(lot);
1219
1598
  return parseTokenSubunits(lot.remainingAmount) - parseTokenSubunits(lot.heldAmount);
@@ -1316,193 +1695,137 @@ function assertDistinctAdjustmentApproval(request) {
1316
1695
  }
1317
1696
  }
1318
1697
 
1319
- // src/projection.ts
1320
- function applyTransactionToProjection(current, transaction) {
1321
- assertBalancedTransaction(transaction);
1322
- const next = { ...current };
1323
- for (const posting of transaction.postings) {
1324
- const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
1325
- next[posting.accountId] = serializeTokenSubunits(
1326
- prior + parseTokenSubunits(posting.amount)
1327
- );
1328
- }
1329
- return next;
1698
+ // src/ports/persistence.ts
1699
+ function assertScopedPersistedIdempotencyResult(result) {
1700
+ economyAssert(
1701
+ result.schemaVersion === ECONOMY_CONTRACT_VERSION,
1702
+ "INVALID_CONTRACT",
1703
+ "Unsupported scoped idempotency-result contract version"
1704
+ );
1705
+ assertEconomyIdentifier(result.commandId, "commandId");
1706
+ assertEconomyIdentifier(result.transactionId, "transactionId");
1707
+ economyAssert(
1708
+ /^sha256:[a-f0-9]{64}$/u.test(result.responseHash),
1709
+ "INVALID_CONTRACT",
1710
+ "Idempotency response hash must be a canonical SHA-256 reference"
1711
+ );
1712
+ parseIsoTimestamp(result.recordedAt);
1330
1713
  }
1331
- function rebuildBalanceProjection(transactions) {
1332
- let projection = {};
1333
- const transactionIds = /* @__PURE__ */ new Set();
1334
- const idempotencyKeys = /* @__PURE__ */ new Set();
1335
- const providerEvents = /* @__PURE__ */ new Set();
1336
- for (const transaction of transactions) {
1337
- economyAssert(
1338
- !transactionIds.has(transaction.transactionId),
1339
- "DUPLICATE_TRANSACTION",
1340
- "Transaction identifiers must be unique"
1341
- );
1714
+ var ECONOMY_COMMAND_TYPES = /* @__PURE__ */ new Set([
1715
+ "credit-purchase",
1716
+ "credit-subscription",
1717
+ "credit-reward",
1718
+ "credit-event",
1719
+ "credit-competition",
1720
+ "allocate",
1721
+ "boost",
1722
+ "reclaim",
1723
+ "spend",
1724
+ "hold",
1725
+ "release-hold",
1726
+ "refund",
1727
+ "chargeback",
1728
+ "reverse",
1729
+ "adjust"
1730
+ ]);
1731
+ function assertEconomyIdempotencyScope(scope) {
1732
+ economyAssert(
1733
+ scope.schemaVersion === ECONOMY_CONTRACT_VERSION && ECONOMY_COMMAND_TYPES.has(scope.commandType),
1734
+ "INVALID_CONTRACT",
1735
+ "Unsupported economy idempotency scope"
1736
+ );
1737
+ assertEconomyIdentifier(scope.idempotencyKey, "idempotencyKey");
1738
+ assertEconomyIdentifier(scope.actorAccountId, "actorAccountId");
1739
+ assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
1740
+ }
1741
+ function assertEconomyCommandEnvelope(envelope) {
1742
+ economyAssert(
1743
+ envelope.schemaVersion === ECONOMY_CONTRACT_VERSION,
1744
+ "INVALID_CONTRACT",
1745
+ "Unsupported economy-command-envelope contract version"
1746
+ );
1747
+ assertEconomyIdentifier(envelope.commandId, "commandId");
1748
+ economyAssert(
1749
+ ECONOMY_COMMAND_TYPES.has(envelope.commandType),
1750
+ "INVALID_CONTRACT",
1751
+ "Economy command has an unsupported type"
1752
+ );
1753
+ assertEconomyIdentifier(envelope.idempotencyKey, "idempotencyKey");
1754
+ assertEconomyIdentifier(envelope.actorAccountId, "actorAccountId");
1755
+ assertEconomyIdentifier(envelope.subjectAccountId, "subjectAccountId");
1756
+ if (envelope.relationshipId !== void 0) {
1757
+ assertEconomyIdentifier(envelope.relationshipId, "relationshipId");
1758
+ }
1759
+ if (envelope.authorizationVersion !== void 0) {
1342
1760
  economyAssert(
1343
- !idempotencyKeys.has(transaction.idempotencyKey),
1344
- "DUPLICATE_TRANSACTION",
1345
- "Idempotency keys must be unique"
1346
- );
1347
- if (transaction.providerEventId !== void 0) {
1348
- economyAssert(
1349
- !providerEvents.has(transaction.providerEventId),
1350
- "DUPLICATE_TRANSACTION",
1351
- "Provider event identifiers must be unique"
1352
- );
1353
- providerEvents.add(transaction.providerEventId);
1354
- }
1355
- transactionIds.add(transaction.transactionId);
1356
- idempotencyKeys.add(transaction.idempotencyKey);
1357
- projection = applyTransactionToProjection(projection, transaction);
1358
- }
1359
- return projection;
1360
- }
1361
- function assertNonNegativeAccounts(projection, protectedAccountIds) {
1362
- for (const accountId of protectedAccountIds) {
1363
- economyAssert(
1364
- parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
1365
- "NEGATIVE_PROJECTION",
1366
- "A protected economy account would become negative"
1761
+ Number.isSafeInteger(envelope.authorizationVersion) && envelope.authorizationVersion >= 1,
1762
+ "INVALID_CONTRACT",
1763
+ "Command authorization version must be a positive safe integer"
1367
1764
  );
1368
1765
  }
1369
- }
1370
- function createProjectionSnapshot(transactions, rebuiltAt) {
1371
- parseIsoTimestamp(rebuiltAt);
1372
- const last = transactions.at(-1);
1373
- return {
1374
- schemaVersion: ECONOMY_CONTRACT_VERSION,
1375
- balances: rebuildBalanceProjection(transactions),
1376
- ...last === void 0 ? {} : { lastTransactionId: last.transactionId },
1377
- rebuiltAt
1378
- };
1379
- }
1380
-
1381
- // src/recurrence.ts
1382
- var BASELINE_MONTHLY_SUBSCRIPTION_PLAN = Object.freeze({
1383
- schemaVersion: ECONOMY_CONTRACT_VERSION,
1384
- planId: "monthly-10-100-v1",
1385
- catalogVersion: "gbp-subscriptions-v1",
1386
- interval: "monthly",
1387
- priceMinorUnits: "1000",
1388
- currency: "GBP",
1389
- grantAmount: serializeTokenSubunits(100000n),
1390
- enabled: false,
1391
- effectiveFrom: "2026-07-15T00:00:00.000Z"
1392
- });
1393
- function assertTokenSubscriptionPlan(plan) {
1394
1766
  economyAssert(
1395
- plan.schemaVersion === ECONOMY_CONTRACT_VERSION,
1767
+ envelope.relationshipId === void 0 === (envelope.authorizationVersion === void 0),
1396
1768
  "INVALID_CONTRACT",
1397
- "Unsupported subscription-plan contract version"
1769
+ "Relationship identity and authorization version must be supplied together"
1398
1770
  );
1399
- assertEconomyIdentifier(plan.planId, "planId");
1400
- assertEconomyIdentifier(plan.catalogVersion, "catalogVersion");
1401
1771
  economyAssert(
1402
- ["daily", "weekly", "monthly"].includes(plan.interval) && plan.currency === "GBP" && typeof plan.enabled === "boolean",
1772
+ /^sha256:[a-f0-9]{64}$/u.test(envelope.payloadHash),
1403
1773
  "INVALID_CONTRACT",
1404
- "Subscription recurrence, currency, or enabled state is invalid"
1405
- );
1406
- economyAssert(
1407
- /^(?:0|[1-9][0-9]*)$/u.test(plan.priceMinorUnits) && BigInt(plan.priceMinorUnits) > 0n,
1408
- "INVALID_AMOUNT",
1409
- "Subscription price must be a positive integer minor-unit string"
1774
+ "Command payload hash must be a canonical SHA-256 reference"
1410
1775
  );
1411
- const grantAmount = parseTokenSubunits(plan.grantAmount);
1412
- economyAssert(
1413
- grantAmount > 0n && isWholeTokenAmount(grantAmount),
1414
- "AMOUNT_NOT_WHOLE_TOKEN",
1415
- "Subscription grants must contain whole Tokens"
1776
+ parseIsoTimestamp(envelope.acceptedAt);
1777
+ assertEconomyIdentifier(envelope.acceptedRegion, "acceptedRegion");
1778
+ assertEconomyIdentifier(
1779
+ envelope.writerFencingToken,
1780
+ "writerFencingToken"
1416
1781
  );
1417
- parseIsoTimestamp(plan.effectiveFrom);
1418
1782
  }
1419
-
1420
- // src/spend-requests.ts
1421
- function assertSpendRequest(request) {
1783
+ function assertEconomyCommandWorkflowEvent(event) {
1422
1784
  economyAssert(
1423
- request.schemaVersion === ECONOMY_CONTRACT_VERSION,
1785
+ event.schemaVersion === ECONOMY_CONTRACT_VERSION,
1424
1786
  "INVALID_CONTRACT",
1425
- "Unsupported spend-request contract version"
1787
+ "Unsupported command-workflow-event contract version"
1426
1788
  );
1427
- assertEconomyIdentifier(request.requestId, "requestId");
1428
- assertEconomyIdentifier(request.childAccountId, "childAccountId");
1429
- assertEconomyIdentifier(request.allocationId, "allocationId");
1789
+ assertEconomyIdentifier(event.workflowEventId, "workflowEventId");
1790
+ assertEconomyIdentifier(event.commandId, "commandId");
1430
1791
  economyAssert(
1431
- request.purpose === "gameplay-conversion" || request.purpose === "non-gameplay",
1792
+ ["accepted", "processing", "failed", "completed"].includes(event.state),
1432
1793
  "INVALID_CONTRACT",
1433
- "Spend request has an unsupported purpose"
1794
+ "Command workflow has an unsupported state"
1434
1795
  );
1435
1796
  economyAssert(
1436
- [
1437
- "requested",
1438
- "approved",
1439
- "rejected",
1440
- "expired",
1441
- "executed",
1442
- "cancelled"
1443
- ].includes(request.status),
1797
+ event.state === "failed" === (event.failureCode !== void 0),
1444
1798
  "INVALID_CONTRACT",
1445
- "Spend request has an unsupported status"
1446
- );
1447
- economyAssert(
1448
- parseTokenSubunits(request.amount) > 0n,
1449
- "INVALID_AMOUNT",
1450
- "Spend-request amount must be positive"
1451
- );
1452
- const requestedAt = parseIsoTimestamp(request.requestedAt);
1453
- const expiresAt = parseIsoTimestamp(request.expiresAt);
1454
- economyAssert(
1455
- expiresAt > requestedAt,
1456
- "INVALID_TIME_WINDOW",
1457
- "Spend-request expiry must follow creation"
1458
- );
1459
- const decisionRequired = ["approved", "rejected", "executed"].includes(
1460
- request.status
1799
+ "Only failed command workflows require a failure code"
1461
1800
  );
1801
+ if (event.failureCode !== void 0) {
1802
+ assertEconomyIdentifier(event.failureCode, "failureCode");
1803
+ }
1804
+ parseIsoTimestamp(event.occurredAt);
1805
+ }
1806
+
1807
+ // src/wallets.ts
1808
+ function assertWalletOwnerReference(owner) {
1462
1809
  economyAssert(
1463
- !decisionRequired || request.decidedByAccountId !== void 0 && request.decidedAt !== void 0,
1810
+ ["account", "household", "system"].includes(owner.ownerType),
1464
1811
  "INVALID_CONTRACT",
1465
- "Decided spend requests require a guardian actor and timestamp"
1812
+ "Wallet owner reference has an unsupported type"
1466
1813
  );
1467
- if (request.decidedByAccountId !== void 0) {
1468
- assertEconomyIdentifier(request.decidedByAccountId, "decidedByAccountId");
1469
- }
1470
- if (request.decidedAt !== void 0) {
1471
- const decidedAt = parseIsoTimestamp(request.decidedAt);
1472
- economyAssert(
1473
- decidedAt >= requestedAt && decidedAt <= expiresAt,
1474
- "INVALID_TIME_WINDOW",
1475
- "Spend-request decision must occur before expiry"
1476
- );
1477
- }
1478
- if (request.approvalNonceHash !== void 0) {
1479
- economyAssert(
1480
- /^sha256:[a-f0-9]{64}$/u.test(request.approvalNonceHash),
1481
- "INVALID_CONTRACT",
1482
- "Approval nonces must be stored as SHA-256 hashes"
1483
- );
1484
- }
1485
- if (request.status === "approved" || request.status === "executed") {
1814
+ if (owner.ownerType === "system") {
1486
1815
  economyAssert(
1487
- request.approvalNonceHash !== void 0,
1816
+ owner.ownerId === "system",
1488
1817
  "INVALID_CONTRACT",
1489
- "Approved spend requests require a one-time approval nonce hash"
1490
- );
1491
- }
1492
- if (request.resultingTransactionId !== void 0) {
1493
- assertEconomyIdentifier(
1494
- request.resultingTransactionId,
1495
- "resultingTransactionId"
1818
+ "System wallet lookups must use the system owner"
1496
1819
  );
1820
+ } else {
1821
+ assertEconomyIdentifier(owner.ownerId, "ownerId");
1497
1822
  economyAssert(
1498
- request.status === "executed",
1823
+ owner.ownerId !== "system",
1499
1824
  "INVALID_CONTRACT",
1500
- "Only executed spend requests may reference a ledger transaction"
1825
+ "Account and household wallets cannot use the system owner"
1501
1826
  );
1502
1827
  }
1503
1828
  }
1504
-
1505
- // src/wallets.ts
1506
1829
  function assertWallet(wallet) {
1507
1830
  economyAssert(
1508
1831
  wallet.schemaVersion === ECONOMY_CONTRACT_VERSION,
@@ -1648,6 +1971,920 @@ function assertWalletLifetimeTotals(totals) {
1648
1971
  );
1649
1972
  }
1650
1973
  }
1974
+
1975
+ // src/projection.ts
1976
+ function assertWalletBalanceProjection(projection) {
1977
+ economyAssert(
1978
+ projection.schemaVersion === ECONOMY_CONTRACT_VERSION,
1979
+ "INVALID_CONTRACT",
1980
+ "Unsupported wallet-balance-projection contract version"
1981
+ );
1982
+ assertEconomyIdentifier(projection.walletId, "walletId");
1983
+ for (const amount of [
1984
+ projection.spendable,
1985
+ projection.reserved,
1986
+ projection.held
1987
+ ]) {
1988
+ economyAssert(
1989
+ parseTokenSubunits(amount) >= 0n,
1990
+ "NEGATIVE_PROJECTION",
1991
+ "Wallet projection buckets cannot be negative"
1992
+ );
1993
+ }
1994
+ economyAssert(
1995
+ Number.isSafeInteger(projection.version) && projection.version >= 1,
1996
+ "INVALID_CONTRACT",
1997
+ "Wallet projection version must be a positive safe integer"
1998
+ );
1999
+ if (projection.lastTransactionId !== void 0) {
2000
+ assertEconomyIdentifier(
2001
+ projection.lastTransactionId,
2002
+ "lastTransactionId"
2003
+ );
2004
+ }
2005
+ parseIsoTimestamp(projection.asOf);
2006
+ }
2007
+ function assertWalletBalanceDelta(delta) {
2008
+ economyAssert(
2009
+ delta.schemaVersion === ECONOMY_CONTRACT_VERSION,
2010
+ "INVALID_CONTRACT",
2011
+ "Unsupported wallet-balance-delta contract version"
2012
+ );
2013
+ assertEconomyIdentifier(delta.walletId, "walletId");
2014
+ const values = [
2015
+ parseTokenSubunits(delta.spendableDelta),
2016
+ parseTokenSubunits(delta.reservedDelta),
2017
+ parseTokenSubunits(delta.heldDelta)
2018
+ ];
2019
+ economyAssert(
2020
+ values.some((value) => value !== 0n),
2021
+ "INVALID_AMOUNT",
2022
+ "Wallet balance delta must change at least one bucket"
2023
+ );
2024
+ }
2025
+ function assertWalletBalanceDeltaBatch(transactionId, deltas) {
2026
+ assertEconomyIdentifier(transactionId, "transactionId");
2027
+ economyAssert(
2028
+ deltas.length > 0,
2029
+ "INVALID_CONTRACT",
2030
+ "An economic transaction must update at least one wallet projection"
2031
+ );
2032
+ const walletIds = /* @__PURE__ */ new Set();
2033
+ for (const delta of deltas) {
2034
+ assertWalletBalanceDelta(delta);
2035
+ economyAssert(
2036
+ !walletIds.has(delta.walletId),
2037
+ "DUPLICATE_IDENTIFIER",
2038
+ "A balance-delta batch must update each wallet once"
2039
+ );
2040
+ walletIds.add(delta.walletId);
2041
+ }
2042
+ }
2043
+ function applyWalletBalanceDelta(projection, delta, transactionId, asOf) {
2044
+ assertWalletBalanceProjection(projection);
2045
+ assertWalletBalanceDelta(delta);
2046
+ assertEconomyIdentifier(transactionId, "transactionId");
2047
+ economyAssert(
2048
+ projection.walletId === delta.walletId,
2049
+ "INVALID_CONTRACT",
2050
+ "Wallet balance delta must target its projection wallet"
2051
+ );
2052
+ economyAssert(
2053
+ parseIsoTimestamp(asOf) >= parseIsoTimestamp(projection.asOf),
2054
+ "INVALID_TIME_WINDOW",
2055
+ "Wallet balance update cannot precede its projection"
2056
+ );
2057
+ const next = {
2058
+ ...projection,
2059
+ spendable: serializeTokenSubunits(
2060
+ parseTokenSubunits(projection.spendable) + parseTokenSubunits(delta.spendableDelta)
2061
+ ),
2062
+ reserved: serializeTokenSubunits(
2063
+ parseTokenSubunits(projection.reserved) + parseTokenSubunits(delta.reservedDelta)
2064
+ ),
2065
+ held: serializeTokenSubunits(
2066
+ parseTokenSubunits(projection.held) + parseTokenSubunits(delta.heldDelta)
2067
+ ),
2068
+ version: projection.version + 1,
2069
+ lastTransactionId: transactionId,
2070
+ asOf
2071
+ };
2072
+ assertWalletBalanceProjection(next);
2073
+ return next;
2074
+ }
2075
+ function createWalletBalanceSummary(projection) {
2076
+ assertWalletBalanceProjection(projection);
2077
+ const spendable = parseTokenSubunits(projection.spendable);
2078
+ const rewardProgress = spendable % 1000n;
2079
+ const summary = {
2080
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2081
+ walletId: projection.walletId,
2082
+ available: serializeTokenSubunits(spendable - rewardProgress),
2083
+ reserved: projection.reserved,
2084
+ held: projection.held,
2085
+ rewardProgress: serializeTokenSubunits(rewardProgress),
2086
+ version: projection.version,
2087
+ asOf: projection.asOf
2088
+ };
2089
+ assertDeterministicWalletBalanceSummary(summary);
2090
+ return summary;
2091
+ }
2092
+ function assertDeterministicWalletBalanceSummary(summary) {
2093
+ assertWalletBalanceSummary(summary);
2094
+ economyAssert(
2095
+ parseTokenSubunits(summary.available) % 1000n === 0n && parseTokenSubunits(summary.rewardProgress) < 1000n,
2096
+ "INVALID_CONTRACT",
2097
+ "Per-wallet availability must be whole Tokens with sub-Token progress below one Token"
2098
+ );
2099
+ }
2100
+ function deriveWalletBalanceDeltas(transaction, bindings) {
2101
+ assertEconomicJournalTransaction(transaction);
2102
+ const byAccount = /* @__PURE__ */ new Map();
2103
+ for (const binding of bindings) {
2104
+ assertEconomyIdentifier(binding.accountId, "accountId");
2105
+ assertEconomyIdentifier(binding.walletId, "walletId");
2106
+ economyAssert(
2107
+ ["spendable", "reserved", "held"].includes(binding.bucket),
2108
+ "INVALID_CONTRACT",
2109
+ "Wallet account binding has an unsupported bucket"
2110
+ );
2111
+ economyAssert(
2112
+ !byAccount.has(binding.accountId),
2113
+ "DUPLICATE_IDENTIFIER",
2114
+ "A ledger account may bind to only one wallet bucket"
2115
+ );
2116
+ byAccount.set(binding.accountId, binding);
2117
+ }
2118
+ const byWallet = /* @__PURE__ */ new Map();
2119
+ for (const posting of transaction.postings) {
2120
+ if (posting.walletId === void 0) {
2121
+ continue;
2122
+ }
2123
+ const binding = byAccount.get(posting.accountId);
2124
+ economyAssert(
2125
+ binding !== void 0 && binding.walletId === posting.walletId,
2126
+ "INVALID_CONTRACT",
2127
+ "Every wallet-linked posting must match an authoritative account binding"
2128
+ );
2129
+ const values = byWallet.get(binding.walletId) ?? [0n, 0n, 0n];
2130
+ const amount = parseTokenSubunits(posting.amount);
2131
+ if (binding.bucket === "spendable") {
2132
+ values[0] += amount;
2133
+ } else if (binding.bucket === "reserved") {
2134
+ values[1] += amount;
2135
+ } else {
2136
+ values[2] += amount;
2137
+ }
2138
+ byWallet.set(binding.walletId, values);
2139
+ }
2140
+ const deltas = [...byWallet.entries()].map(([walletId, values]) => ({
2141
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2142
+ walletId,
2143
+ spendableDelta: serializeTokenSubunits(values[0]),
2144
+ reservedDelta: serializeTokenSubunits(values[1]),
2145
+ heldDelta: serializeTokenSubunits(values[2])
2146
+ })).filter(
2147
+ (delta) => delta.spendableDelta !== "0" || delta.reservedDelta !== "0" || delta.heldDelta !== "0"
2148
+ ).sort((left, right) => left.walletId.localeCompare(right.walletId));
2149
+ assertWalletBalanceDeltaBatch(transaction.transactionId, deltas);
2150
+ return deltas;
2151
+ }
2152
+ var EARNED_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
2153
+ "rewarded-ad",
2154
+ "offerwall",
2155
+ "event",
2156
+ "competition"
2157
+ ]);
2158
+ function deriveWalletLifetimeDeltas(transaction) {
2159
+ assertEconomicJournalTransaction(transaction);
2160
+ if (transaction.status !== "settled") {
2161
+ return [];
2162
+ }
2163
+ const netByWallet = /* @__PURE__ */ new Map();
2164
+ for (const posting of transaction.postings) {
2165
+ if (posting.walletId !== void 0) {
2166
+ netByWallet.set(
2167
+ posting.walletId,
2168
+ (netByWallet.get(posting.walletId) ?? 0n) + parseTokenSubunits(posting.amount)
2169
+ );
2170
+ }
2171
+ }
2172
+ const deltas = [];
2173
+ for (const [walletId, net] of [...netByWallet.entries()].sort(
2174
+ ([left], [right]) => left.localeCompare(right)
2175
+ )) {
2176
+ let bucket;
2177
+ let amount = 0n;
2178
+ if (net > 0n && (transaction.activityType === "purchase" || transaction.activityType === "subscription")) {
2179
+ bucket = "bought";
2180
+ amount = net;
2181
+ } else if (net > 0n && EARNED_ACTIVITY_TYPES.has(transaction.activityType)) {
2182
+ bucket = "earned";
2183
+ amount = net;
2184
+ } else if (net < 0n && (transaction.activityType === "allocation" || transaction.activityType === "boost")) {
2185
+ bucket = "allocated";
2186
+ amount = -net;
2187
+ } else if (net > 0n && transaction.activityType === "reclaim") {
2188
+ bucket = "reclaimed";
2189
+ amount = net;
2190
+ } else if (net < 0n && transaction.activityType === "spend") {
2191
+ bucket = "spent";
2192
+ amount = -net;
2193
+ } else if (net < 0n && (transaction.activityType === "refund" || transaction.activityType === "chargeback" || transaction.activityType === "reversal")) {
2194
+ bucket = "reversed";
2195
+ amount = -net;
2196
+ }
2197
+ if (bucket !== void 0) {
2198
+ deltas.push({
2199
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2200
+ walletId,
2201
+ bucket,
2202
+ amount: serializeTokenSubunits(amount)
2203
+ });
2204
+ }
2205
+ }
2206
+ return deltas;
2207
+ }
2208
+ function applyWalletLifetimeDeltas(current, walletId, deltas) {
2209
+ assertWalletLifetimeTotals(current);
2210
+ assertEconomyIdentifier(walletId, "walletId");
2211
+ const next = {
2212
+ bought: parseTokenSubunits(current.bought),
2213
+ earned: parseTokenSubunits(current.earned),
2214
+ allocated: parseTokenSubunits(current.allocated),
2215
+ reclaimed: parseTokenSubunits(current.reclaimed),
2216
+ spent: parseTokenSubunits(current.spent),
2217
+ reversed: parseTokenSubunits(current.reversed)
2218
+ };
2219
+ for (const delta of deltas) {
2220
+ economyAssert(
2221
+ delta.schemaVersion === ECONOMY_CONTRACT_VERSION && delta.walletId === walletId && [
2222
+ "bought",
2223
+ "earned",
2224
+ "allocated",
2225
+ "reclaimed",
2226
+ "spent",
2227
+ "reversed"
2228
+ ].includes(delta.bucket),
2229
+ "INVALID_CONTRACT",
2230
+ "Lifetime delta must target the requested wallet and a known bucket"
2231
+ );
2232
+ const amount = parseTokenSubunits(delta.amount);
2233
+ economyAssert(
2234
+ amount > 0n,
2235
+ "INVALID_AMOUNT",
2236
+ "Lifetime deltas must be positive"
2237
+ );
2238
+ next[delta.bucket] += amount;
2239
+ }
2240
+ const totals = {
2241
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2242
+ bought: serializeTokenSubunits(next.bought),
2243
+ earned: serializeTokenSubunits(next.earned),
2244
+ allocated: serializeTokenSubunits(next.allocated),
2245
+ reclaimed: serializeTokenSubunits(next.reclaimed),
2246
+ spent: serializeTokenSubunits(next.spent),
2247
+ reversed: serializeTokenSubunits(next.reversed)
2248
+ };
2249
+ assertWalletLifetimeTotals(totals);
2250
+ return totals;
2251
+ }
2252
+ function applyTransactionToProjection(current, transaction) {
2253
+ assertBalancedTransaction(transaction);
2254
+ const next = { ...current };
2255
+ for (const posting of transaction.postings) {
2256
+ const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
2257
+ next[posting.accountId] = serializeTokenSubunits(
2258
+ prior + parseTokenSubunits(posting.amount)
2259
+ );
2260
+ }
2261
+ return next;
2262
+ }
2263
+ function rebuildBalanceProjection(transactions) {
2264
+ let projection = {};
2265
+ const transactionIds = /* @__PURE__ */ new Set();
2266
+ const idempotencyKeys = /* @__PURE__ */ new Set();
2267
+ const providerEvents = /* @__PURE__ */ new Set();
2268
+ for (const transaction of transactions) {
2269
+ economyAssert(
2270
+ !transactionIds.has(transaction.transactionId),
2271
+ "DUPLICATE_TRANSACTION",
2272
+ "Transaction identifiers must be unique"
2273
+ );
2274
+ economyAssert(
2275
+ !idempotencyKeys.has(transaction.idempotencyKey),
2276
+ "DUPLICATE_TRANSACTION",
2277
+ "Idempotency keys must be unique"
2278
+ );
2279
+ if (transaction.providerEventId !== void 0) {
2280
+ economyAssert(
2281
+ !providerEvents.has(transaction.providerEventId),
2282
+ "DUPLICATE_TRANSACTION",
2283
+ "Provider event identifiers must be unique"
2284
+ );
2285
+ providerEvents.add(transaction.providerEventId);
2286
+ }
2287
+ transactionIds.add(transaction.transactionId);
2288
+ idempotencyKeys.add(transaction.idempotencyKey);
2289
+ projection = applyTransactionToProjection(projection, transaction);
2290
+ }
2291
+ return projection;
2292
+ }
2293
+ function assertNonNegativeAccounts(projection, protectedAccountIds) {
2294
+ for (const accountId of protectedAccountIds) {
2295
+ economyAssert(
2296
+ parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
2297
+ "NEGATIVE_PROJECTION",
2298
+ "A protected economy account would become negative"
2299
+ );
2300
+ }
2301
+ }
2302
+ function createProjectionSnapshot(transactions, rebuiltAt) {
2303
+ parseIsoTimestamp(rebuiltAt);
2304
+ const last = transactions.at(-1);
2305
+ return {
2306
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2307
+ balances: rebuildBalanceProjection(transactions),
2308
+ ...last === void 0 ? {} : { lastTransactionId: last.transactionId },
2309
+ rebuiltAt
2310
+ };
2311
+ }
2312
+
2313
+ // src/ports/query.ts
2314
+ var ACTIVITY_TYPES2 = /* @__PURE__ */ new Set([
2315
+ "purchase",
2316
+ "subscription",
2317
+ "rewarded-ad",
2318
+ "offerwall",
2319
+ "allocation",
2320
+ "boost",
2321
+ "reclaim",
2322
+ "spend",
2323
+ "hold",
2324
+ "refund",
2325
+ "chargeback",
2326
+ "adjustment",
2327
+ "reversal",
2328
+ "event",
2329
+ "competition"
2330
+ ]);
2331
+ var ACTIVITY_STATUSES2 = /* @__PURE__ */ new Set([
2332
+ "pending",
2333
+ "held",
2334
+ "settled",
2335
+ "reversed",
2336
+ "failed"
2337
+ ]);
2338
+ var ACTIVITY_SOURCES2 = /* @__PURE__ */ new Set([
2339
+ "shopify",
2340
+ "ayet",
2341
+ "bitlabs",
2342
+ "subscription",
2343
+ "event",
2344
+ "competition",
2345
+ "adjustment"
2346
+ ]);
2347
+ var PORTFOLIO_ROLES = /* @__PURE__ */ new Set([
2348
+ "household-treasury",
2349
+ "personal",
2350
+ "gameplay-allocation",
2351
+ "hold"
2352
+ ]);
2353
+ function hasControlCharacters2(value) {
2354
+ return [...value].some((character) => {
2355
+ const code = character.charCodeAt(0);
2356
+ return code <= 31 || code === 127;
2357
+ });
2358
+ }
2359
+ function assertSafeDisplayString(value, label) {
2360
+ economyAssert(
2361
+ typeof value === "string" && value.trim().length > 0 && value.length <= 128 && !hasControlCharacters2(value),
2362
+ "INVALID_CONTRACT",
2363
+ `${label} must be bounded and safe for display`
2364
+ );
2365
+ }
2366
+ function assertOpaqueCursor(value) {
2367
+ economyAssert(
2368
+ typeof value === "string" && value.length > 0 && value.length <= 512 && !hasControlCharacters2(value),
2369
+ "INVALID_CONTRACT",
2370
+ "Activity cursor must be a bounded opaque value"
2371
+ );
2372
+ }
2373
+ function assertUniqueValues(values, allowed, label) {
2374
+ const unique = new Set(values);
2375
+ economyAssert(
2376
+ values.length > 0 && values.length === unique.size && values.every((value) => allowed.has(value)),
2377
+ "INVALID_CONTRACT",
2378
+ `${label} must contain unique supported values`
2379
+ );
2380
+ }
2381
+ function assertWalletPortfolioReadScope(scope) {
2382
+ economyAssert(
2383
+ scope.schemaVersion === ECONOMY_CONTRACT_VERSION,
2384
+ "INVALID_CONTRACT",
2385
+ "Unsupported wallet-portfolio scope contract version"
2386
+ );
2387
+ assertEconomyIdentifier(scope.portfolioId, "portfolioId");
2388
+ assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
2389
+ economyAssert(
2390
+ scope.components.length > 0 && scope.components.length <= 100,
2391
+ "INVALID_CONTRACT",
2392
+ "Wallet portfolio must contain between one and 100 components"
2393
+ );
2394
+ const walletIds = /* @__PURE__ */ new Set();
2395
+ for (const component of scope.components) {
2396
+ assertEconomyIdentifier(component.walletId, "walletId");
2397
+ economyAssert(
2398
+ PORTFOLIO_ROLES.has(component.role),
2399
+ "INVALID_CONTRACT",
2400
+ "Wallet portfolio component has an unsupported role"
2401
+ );
2402
+ economyAssert(
2403
+ !walletIds.has(component.walletId),
2404
+ "DUPLICATE_IDENTIFIER",
2405
+ "Wallet portfolio cannot repeat a wallet"
2406
+ );
2407
+ walletIds.add(component.walletId);
2408
+ if (component.beneficiaryAccountId !== void 0) {
2409
+ assertEconomyIdentifier(
2410
+ component.beneficiaryAccountId,
2411
+ "beneficiaryAccountId"
2412
+ );
2413
+ }
2414
+ economyAssert(
2415
+ component.role !== "gameplay-allocation" || component.beneficiaryAccountId !== void 0,
2416
+ "INVALID_CONTRACT",
2417
+ "Gameplay-allocation portfolio components require a beneficiary"
2418
+ );
2419
+ }
2420
+ }
2421
+ function assertWalletLifetimeSnapshot(snapshot) {
2422
+ economyAssert(
2423
+ snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
2424
+ "INVALID_CONTRACT",
2425
+ "Unsupported wallet-lifetime-snapshot contract version"
2426
+ );
2427
+ assertEconomyIdentifier(snapshot.walletId, "walletId");
2428
+ assertWalletLifetimeTotals(snapshot.totals);
2429
+ economyAssert(
2430
+ Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
2431
+ "INVALID_CONTRACT",
2432
+ "Wallet lifetime version must be a positive safe integer"
2433
+ );
2434
+ parseIsoTimestamp(snapshot.asOf);
2435
+ }
2436
+ function assertPortfolioComponentMatch(expected, actual) {
2437
+ economyAssert(
2438
+ expected.walletId === actual.walletId && expected.role === actual.role && expected.beneficiaryAccountId === actual.beneficiaryAccountId,
2439
+ "INVALID_CONTRACT",
2440
+ "Portfolio result component does not match its authorized read scope"
2441
+ );
2442
+ }
2443
+ function createWalletPortfolioSummary(scope, components, asOf) {
2444
+ assertWalletPortfolioReadScope(scope);
2445
+ parseIsoTimestamp(asOf);
2446
+ economyAssert(
2447
+ components.length === scope.components.length,
2448
+ "INVALID_CONTRACT",
2449
+ "Portfolio balance must include every authorized component exactly once"
2450
+ );
2451
+ const byWallet = new Map(components.map((item) => [item.walletId, item]));
2452
+ economyAssert(
2453
+ byWallet.size === components.length,
2454
+ "DUPLICATE_IDENTIFIER",
2455
+ "Portfolio balance cannot repeat a wallet"
2456
+ );
2457
+ const ordered = scope.components.map((expected) => {
2458
+ const actual = byWallet.get(expected.walletId);
2459
+ economyAssert(
2460
+ actual !== void 0,
2461
+ "INVALID_CONTRACT",
2462
+ "Portfolio balance is missing an authorized wallet"
2463
+ );
2464
+ assertPortfolioComponentMatch(expected, actual);
2465
+ assertDeterministicWalletBalanceSummary(actual.summary);
2466
+ economyAssert(
2467
+ actual.summary.walletId === actual.walletId && actual.summary.asOf === asOf,
2468
+ "INVALID_CONTRACT",
2469
+ "Portfolio component summary must identify its wallet and shared snapshot time"
2470
+ );
2471
+ return actual;
2472
+ });
2473
+ const totals = {
2474
+ available: serializeTokenSubunits(
2475
+ ordered.reduce(
2476
+ (sum, item) => sum + parseTokenSubunits(item.summary.available),
2477
+ 0n
2478
+ )
2479
+ ),
2480
+ reserved: serializeTokenSubunits(
2481
+ ordered.reduce(
2482
+ (sum, item) => sum + parseTokenSubunits(item.summary.reserved),
2483
+ 0n
2484
+ )
2485
+ ),
2486
+ held: serializeTokenSubunits(
2487
+ ordered.reduce(
2488
+ (sum, item) => sum + parseTokenSubunits(item.summary.held),
2489
+ 0n
2490
+ )
2491
+ ),
2492
+ rewardProgress: serializeTokenSubunits(
2493
+ ordered.reduce(
2494
+ (sum, item) => sum + parseTokenSubunits(item.summary.rewardProgress),
2495
+ 0n
2496
+ )
2497
+ )
2498
+ };
2499
+ return {
2500
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2501
+ portfolioId: scope.portfolioId,
2502
+ subjectAccountId: scope.subjectAccountId,
2503
+ components: ordered,
2504
+ totals,
2505
+ asOf
2506
+ };
2507
+ }
2508
+ function assertWalletPortfolioSummary(summary) {
2509
+ const scope = {
2510
+ schemaVersion: summary.schemaVersion,
2511
+ portfolioId: summary.portfolioId,
2512
+ subjectAccountId: summary.subjectAccountId,
2513
+ components: summary.components.map(
2514
+ ({ walletId, role, beneficiaryAccountId }) => ({
2515
+ walletId,
2516
+ role,
2517
+ ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
2518
+ })
2519
+ )
2520
+ };
2521
+ const rebuilt = createWalletPortfolioSummary(
2522
+ scope,
2523
+ summary.components,
2524
+ summary.asOf
2525
+ );
2526
+ for (const field of [
2527
+ "available",
2528
+ "reserved",
2529
+ "held",
2530
+ "rewardProgress"
2531
+ ]) {
2532
+ economyAssert(
2533
+ summary.totals[field] === rebuilt.totals[field],
2534
+ "INVALID_CONTRACT",
2535
+ "Portfolio balance totals must equal their component column sums"
2536
+ );
2537
+ }
2538
+ }
2539
+ function createWalletPortfolioLifetime(scope, components, asOf) {
2540
+ assertWalletPortfolioReadScope(scope);
2541
+ parseIsoTimestamp(asOf);
2542
+ economyAssert(
2543
+ components.length === scope.components.length,
2544
+ "INVALID_CONTRACT",
2545
+ "Portfolio lifetime must include every authorized component exactly once"
2546
+ );
2547
+ const byWallet = new Map(components.map((item) => [item.walletId, item]));
2548
+ economyAssert(
2549
+ byWallet.size === components.length,
2550
+ "DUPLICATE_IDENTIFIER",
2551
+ "Portfolio lifetime cannot repeat a wallet"
2552
+ );
2553
+ const ordered = scope.components.map((expected) => {
2554
+ const actual = byWallet.get(expected.walletId);
2555
+ economyAssert(
2556
+ actual !== void 0,
2557
+ "INVALID_CONTRACT",
2558
+ "Portfolio lifetime is missing an authorized wallet"
2559
+ );
2560
+ assertPortfolioComponentMatch(expected, actual);
2561
+ assertWalletLifetimeSnapshot(actual.snapshot);
2562
+ economyAssert(
2563
+ actual.snapshot.walletId === actual.walletId && actual.snapshot.asOf === asOf,
2564
+ "INVALID_CONTRACT",
2565
+ "Portfolio lifetime component must identify its wallet and shared snapshot time"
2566
+ );
2567
+ return actual;
2568
+ });
2569
+ const sumField = (field) => serializeTokenSubunits(
2570
+ ordered.reduce(
2571
+ (sum, item) => sum + parseTokenSubunits(item.snapshot.totals[field]),
2572
+ 0n
2573
+ )
2574
+ );
2575
+ const totals = {
2576
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2577
+ bought: sumField("bought"),
2578
+ earned: sumField("earned"),
2579
+ allocated: sumField("allocated"),
2580
+ reclaimed: sumField("reclaimed"),
2581
+ spent: sumField("spent"),
2582
+ reversed: sumField("reversed")
2583
+ };
2584
+ assertWalletLifetimeTotals(totals);
2585
+ return {
2586
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2587
+ portfolioId: scope.portfolioId,
2588
+ subjectAccountId: scope.subjectAccountId,
2589
+ components: ordered,
2590
+ totals,
2591
+ asOf
2592
+ };
2593
+ }
2594
+ function assertWalletPortfolioLifetime(lifetime) {
2595
+ const scope = {
2596
+ schemaVersion: lifetime.schemaVersion,
2597
+ portfolioId: lifetime.portfolioId,
2598
+ subjectAccountId: lifetime.subjectAccountId,
2599
+ components: lifetime.components.map(
2600
+ ({ walletId, role, beneficiaryAccountId }) => ({
2601
+ walletId,
2602
+ role,
2603
+ ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
2604
+ })
2605
+ )
2606
+ };
2607
+ const rebuilt = createWalletPortfolioLifetime(
2608
+ scope,
2609
+ lifetime.components,
2610
+ lifetime.asOf
2611
+ );
2612
+ for (const field of [
2613
+ "bought",
2614
+ "earned",
2615
+ "allocated",
2616
+ "reclaimed",
2617
+ "spent",
2618
+ "reversed"
2619
+ ]) {
2620
+ economyAssert(
2621
+ lifetime.totals[field] === rebuilt.totals[field],
2622
+ "INVALID_CONTRACT",
2623
+ "Portfolio lifetime totals must equal their component column sums"
2624
+ );
2625
+ }
2626
+ }
2627
+ function assertWalletActivityEntry(entry) {
2628
+ economyAssert(
2629
+ entry.schemaVersion === ECONOMY_CONTRACT_VERSION,
2630
+ "INVALID_CONTRACT",
2631
+ "Unsupported wallet-activity contract version"
2632
+ );
2633
+ assertEconomyIdentifier(entry.activityId, "activityId");
2634
+ assertEconomyIdentifier(entry.walletId, "walletId");
2635
+ economyAssert(
2636
+ ACTIVITY_TYPES2.has(entry.activityType) && ACTIVITY_STATUSES2.has(entry.status) && ACTIVITY_SOURCES2.has(entry.source),
2637
+ "INVALID_CONTRACT",
2638
+ "Wallet activity type, status, or source is unsupported"
2639
+ );
2640
+ parseIsoTimestamp(entry.occurredAt);
2641
+ economyAssert(
2642
+ parseTokenSubunits(entry.amount) !== 0n,
2643
+ "INVALID_AMOUNT",
2644
+ "Wallet activity amount cannot be zero"
2645
+ );
2646
+ if (entry.beneficiaryAccountId !== void 0) {
2647
+ assertEconomyIdentifier(
2648
+ entry.beneficiaryAccountId,
2649
+ "beneficiaryAccountId"
2650
+ );
2651
+ }
2652
+ if (entry.maskedReference !== void 0) {
2653
+ assertSafeDisplayString(entry.maskedReference, "Masked reference");
2654
+ }
2655
+ assertSafeDisplayString(entry.sourceLabel, "Source label");
2656
+ economyAssert(
2657
+ entry.entryKind === "economic" || entry.entryKind === "workflow",
2658
+ "INVALID_CONTRACT",
2659
+ "Wallet activity has an unsupported entry kind"
2660
+ );
2661
+ if (entry.entryKind === "economic") {
2662
+ assertEconomyIdentifier(entry.transactionId, "transactionId");
2663
+ economyAssert(
2664
+ entry.status === "held" || entry.status === "settled" || entry.status === "reversed",
2665
+ "INVALID_CONTRACT",
2666
+ "Economic activity cannot use a workflow status"
2667
+ );
2668
+ } else {
2669
+ assertEconomyIdentifier(entry.commandId, "commandId");
2670
+ economyAssert(
2671
+ entry.status === "pending" || entry.status === "failed",
2672
+ "INVALID_CONTRACT",
2673
+ "Workflow activity cannot use an economic status"
2674
+ );
2675
+ }
2676
+ }
2677
+ function assertWalletActivityPageRequest(request) {
2678
+ economyAssert(
2679
+ Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 100,
2680
+ "INVALID_CONTRACT",
2681
+ "Activity page limit must be between one and 100"
2682
+ );
2683
+ if (request.cursor !== void 0) {
2684
+ assertOpaqueCursor(request.cursor);
2685
+ }
2686
+ const filter = request.filter;
2687
+ if (filter === void 0) {
2688
+ return;
2689
+ }
2690
+ if (filter.activityTypes !== void 0) {
2691
+ assertUniqueValues(filter.activityTypes, ACTIVITY_TYPES2, "Activity types");
2692
+ }
2693
+ if (filter.statuses !== void 0) {
2694
+ assertUniqueValues(filter.statuses, ACTIVITY_STATUSES2, "Activity statuses");
2695
+ }
2696
+ if (filter.sources !== void 0) {
2697
+ assertUniqueValues(filter.sources, ACTIVITY_SOURCES2, "Activity sources");
2698
+ }
2699
+ if (filter.beneficiaryAccountId !== void 0) {
2700
+ assertEconomyIdentifier(
2701
+ filter.beneficiaryAccountId,
2702
+ "beneficiaryAccountId"
2703
+ );
2704
+ }
2705
+ const from = filter.fromInclusive === void 0 ? void 0 : parseIsoTimestamp(filter.fromInclusive);
2706
+ const to = filter.toExclusive === void 0 ? void 0 : parseIsoTimestamp(filter.toExclusive);
2707
+ economyAssert(
2708
+ from === void 0 || to === void 0 || from < to,
2709
+ "INVALID_TIME_WINDOW",
2710
+ "Activity filter start must precede its exclusive end"
2711
+ );
2712
+ }
2713
+ function assertWalletActivityPage(page) {
2714
+ economyAssert(
2715
+ page.schemaVersion === ECONOMY_CONTRACT_VERSION && typeof page.hasMore === "boolean",
2716
+ "INVALID_CONTRACT",
2717
+ "Unsupported wallet-activity-page contract"
2718
+ );
2719
+ economyAssert(
2720
+ page.hasMore === (page.nextCursor !== void 0),
2721
+ "INVALID_CONTRACT",
2722
+ "Activity page cursor must be present exactly when more rows exist"
2723
+ );
2724
+ economyAssert(
2725
+ page.entries.length <= 100 && (!page.hasMore || page.entries.length > 0),
2726
+ "INVALID_CONTRACT",
2727
+ "Activity pages are bounded and cannot advertise more rows from an empty page"
2728
+ );
2729
+ if (page.nextCursor !== void 0) {
2730
+ assertOpaqueCursor(page.nextCursor);
2731
+ }
2732
+ const activityIds = /* @__PURE__ */ new Set();
2733
+ let previous;
2734
+ for (const entry of page.entries) {
2735
+ assertWalletActivityEntry(entry);
2736
+ economyAssert(
2737
+ !activityIds.has(entry.activityId),
2738
+ "DUPLICATE_IDENTIFIER",
2739
+ "Activity page cannot repeat an activity ID"
2740
+ );
2741
+ activityIds.add(entry.activityId);
2742
+ if (previous !== void 0) {
2743
+ const previousTime = parseIsoTimestamp(previous.occurredAt);
2744
+ const currentTime = parseIsoTimestamp(entry.occurredAt);
2745
+ economyAssert(
2746
+ previousTime > currentTime || previousTime === currentTime && previous.activityId.localeCompare(entry.activityId) > 0,
2747
+ "INVALID_CONTRACT",
2748
+ "Activity page must use stable descending time and ID order"
2749
+ );
2750
+ }
2751
+ previous = entry;
2752
+ }
2753
+ }
2754
+ function assertWalletActivityPageForPortfolio(page, scope) {
2755
+ assertWalletActivityPage(page);
2756
+ assertWalletPortfolioReadScope(scope);
2757
+ const walletIds = new Set(scope.components.map((item) => item.walletId));
2758
+ economyAssert(
2759
+ page.entries.every((entry) => walletIds.has(entry.walletId)),
2760
+ "INVALID_CONTRACT",
2761
+ "Portfolio activity contains a wallet outside its authorized read scope"
2762
+ );
2763
+ }
2764
+
2765
+ // src/recurrence.ts
2766
+ var BASELINE_MONTHLY_SUBSCRIPTION_PLAN = Object.freeze({
2767
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2768
+ planId: "monthly-10-100-v1",
2769
+ catalogVersion: "gbp-subscriptions-v1",
2770
+ interval: "monthly",
2771
+ priceMinorUnits: "1000",
2772
+ currency: "GBP",
2773
+ grantAmount: serializeTokenSubunits(100000n),
2774
+ enabled: false,
2775
+ effectiveFrom: "2026-07-15T00:00:00.000Z"
2776
+ });
2777
+ function assertTokenSubscriptionPlan(plan) {
2778
+ economyAssert(
2779
+ plan.schemaVersion === ECONOMY_CONTRACT_VERSION,
2780
+ "INVALID_CONTRACT",
2781
+ "Unsupported subscription-plan contract version"
2782
+ );
2783
+ assertEconomyIdentifier(plan.planId, "planId");
2784
+ assertEconomyIdentifier(plan.catalogVersion, "catalogVersion");
2785
+ economyAssert(
2786
+ ["daily", "weekly", "monthly"].includes(plan.interval) && plan.currency === "GBP" && typeof plan.enabled === "boolean",
2787
+ "INVALID_CONTRACT",
2788
+ "Subscription recurrence, currency, or enabled state is invalid"
2789
+ );
2790
+ economyAssert(
2791
+ /^(?:0|[1-9][0-9]*)$/u.test(plan.priceMinorUnits) && BigInt(plan.priceMinorUnits) > 0n,
2792
+ "INVALID_AMOUNT",
2793
+ "Subscription price must be a positive integer minor-unit string"
2794
+ );
2795
+ const grantAmount = parseTokenSubunits(plan.grantAmount);
2796
+ economyAssert(
2797
+ grantAmount > 0n && isWholeTokenAmount(grantAmount),
2798
+ "AMOUNT_NOT_WHOLE_TOKEN",
2799
+ "Subscription grants must contain whole Tokens"
2800
+ );
2801
+ parseIsoTimestamp(plan.effectiveFrom);
2802
+ }
2803
+
2804
+ // src/spend-requests.ts
2805
+ function assertSpendRequest(request) {
2806
+ economyAssert(
2807
+ request.schemaVersion === ECONOMY_CONTRACT_VERSION,
2808
+ "INVALID_CONTRACT",
2809
+ "Unsupported spend-request contract version"
2810
+ );
2811
+ assertEconomyIdentifier(request.requestId, "requestId");
2812
+ assertEconomyIdentifier(request.childAccountId, "childAccountId");
2813
+ assertEconomyIdentifier(request.allocationId, "allocationId");
2814
+ economyAssert(
2815
+ request.purpose === "gameplay-conversion" || request.purpose === "non-gameplay",
2816
+ "INVALID_CONTRACT",
2817
+ "Spend request has an unsupported purpose"
2818
+ );
2819
+ economyAssert(
2820
+ [
2821
+ "requested",
2822
+ "approved",
2823
+ "rejected",
2824
+ "expired",
2825
+ "executed",
2826
+ "cancelled"
2827
+ ].includes(request.status),
2828
+ "INVALID_CONTRACT",
2829
+ "Spend request has an unsupported status"
2830
+ );
2831
+ economyAssert(
2832
+ parseTokenSubunits(request.amount) > 0n,
2833
+ "INVALID_AMOUNT",
2834
+ "Spend-request amount must be positive"
2835
+ );
2836
+ const requestedAt = parseIsoTimestamp(request.requestedAt);
2837
+ const expiresAt = parseIsoTimestamp(request.expiresAt);
2838
+ economyAssert(
2839
+ expiresAt > requestedAt,
2840
+ "INVALID_TIME_WINDOW",
2841
+ "Spend-request expiry must follow creation"
2842
+ );
2843
+ const decisionRequired = ["approved", "rejected", "executed"].includes(
2844
+ request.status
2845
+ );
2846
+ economyAssert(
2847
+ !decisionRequired || request.decidedByAccountId !== void 0 && request.decidedAt !== void 0,
2848
+ "INVALID_CONTRACT",
2849
+ "Decided spend requests require a guardian actor and timestamp"
2850
+ );
2851
+ if (request.decidedByAccountId !== void 0) {
2852
+ assertEconomyIdentifier(request.decidedByAccountId, "decidedByAccountId");
2853
+ }
2854
+ if (request.decidedAt !== void 0) {
2855
+ const decidedAt = parseIsoTimestamp(request.decidedAt);
2856
+ economyAssert(
2857
+ decidedAt >= requestedAt && decidedAt <= expiresAt,
2858
+ "INVALID_TIME_WINDOW",
2859
+ "Spend-request decision must occur before expiry"
2860
+ );
2861
+ }
2862
+ if (request.approvalNonceHash !== void 0) {
2863
+ economyAssert(
2864
+ /^sha256:[a-f0-9]{64}$/u.test(request.approvalNonceHash),
2865
+ "INVALID_CONTRACT",
2866
+ "Approval nonces must be stored as SHA-256 hashes"
2867
+ );
2868
+ }
2869
+ if (request.status === "approved" || request.status === "executed") {
2870
+ economyAssert(
2871
+ request.approvalNonceHash !== void 0,
2872
+ "INVALID_CONTRACT",
2873
+ "Approved spend requests require a one-time approval nonce hash"
2874
+ );
2875
+ }
2876
+ if (request.resultingTransactionId !== void 0) {
2877
+ assertEconomyIdentifier(
2878
+ request.resultingTransactionId,
2879
+ "resultingTransactionId"
2880
+ );
2881
+ economyAssert(
2882
+ request.status === "executed",
2883
+ "INVALID_CONTRACT",
2884
+ "Only executed spend requests may reference a ledger transaction"
2885
+ );
2886
+ }
2887
+ }
1651
2888
  // Annotate the CommonJS export names for ESM import in node:
1652
2889
  0 && (module.exports = {
1653
2890
  BASELINE_GBP_CATALOG_VERSION,
@@ -1655,20 +2892,31 @@ function assertWalletLifetimeTotals(totals) {
1655
2892
  BASELINE_GBP_TOKEN_PACKS,
1656
2893
  BASELINE_MONTHLY_SUBSCRIPTION_PLAN,
1657
2894
  BASELINE_PURCHASE_LIMIT_POLICY,
2895
+ EARLY_BACKER_SETTLEMENT_POLICY_V2,
1658
2896
  ECONOMY_CONTRACT_VERSION,
1659
2897
  EconomyError,
1660
2898
  PRE_UTILITY_BACKER_COHORT,
1661
2899
  TOKEN_SUBUNITS_MAX,
1662
2900
  TOKEN_SUBUNITS_MIN,
1663
2901
  TOKEN_SUBUNITS_PER_TOKEN,
2902
+ advanceJournalChainHead,
2903
+ applySourceLotMovement,
1664
2904
  applyTransactionToProjection,
2905
+ applyWalletBalanceDelta,
2906
+ applyWalletLifetimeDeltas,
1665
2907
  assertActivityEntry,
1666
2908
  assertBalancedTransaction,
2909
+ assertDeterministicWalletBalanceSummary,
1667
2910
  assertDistinctAdjustmentApproval,
2911
+ assertEconomicJournalTransaction,
2912
+ assertEconomyCommandEnvelope,
2913
+ assertEconomyCommandWorkflowEvent,
2914
+ assertEconomyIdempotencyScope,
1668
2915
  assertEconomyIdentifier,
1669
2916
  assertFlatTokenCatalog,
1670
2917
  assertFxSnapshot,
1671
2918
  assertGameplayAllocation,
2919
+ assertJournalChainHead,
1672
2920
  assertNonNegativeAccounts,
1673
2921
  assertOpenPurchaseIntent,
1674
2922
  assertPurchaseIntentBinding,
@@ -1676,24 +2924,46 @@ function assertWalletLifetimeTotals(totals) {
1676
2924
  assertReversalAvailable,
1677
2925
  assertRewardConversion,
1678
2926
  assertRewardRate,
2927
+ assertScopedPersistedIdempotencyResult,
1679
2928
  assertSourceLot,
2929
+ assertSourceLotMovement,
1680
2930
  assertSpendRequest,
1681
2931
  assertTokenPack,
1682
2932
  assertTokenReferenceRate,
1683
2933
  assertTokenSubscriptionPlan,
2934
+ assertVersionedSourceLot,
1684
2935
  assertWallet,
2936
+ assertWalletActivityEntry,
2937
+ assertWalletActivityPage,
2938
+ assertWalletActivityPageForPortfolio,
2939
+ assertWalletActivityPageRequest,
2940
+ assertWalletBalanceDelta,
2941
+ assertWalletBalanceDeltaBatch,
2942
+ assertWalletBalanceProjection,
1685
2943
  assertWalletBalanceSummary,
2944
+ assertWalletLifetimeSnapshot,
1686
2945
  assertWalletLifetimeTotals,
2946
+ assertWalletOwnerReference,
2947
+ assertWalletPortfolioLifetime,
2948
+ assertWalletPortfolioReadScope,
2949
+ assertWalletPortfolioSummary,
1687
2950
  availableSourceLotAmount,
1688
2951
  boostGameplayAllocation,
1689
2952
  canUseSourceLot,
1690
2953
  canonicalTransactionPayload,
1691
2954
  convertRewardPayout,
1692
2955
  createGameplayAllocation,
2956
+ createInitialSourceLotSnapshot,
1693
2957
  createProjectionSnapshot,
1694
2958
  createReversalTransaction,
2959
+ createWalletBalanceSummary,
2960
+ createWalletPortfolioLifetime,
2961
+ createWalletPortfolioSummary,
2962
+ deriveWalletBalanceDeltas,
2963
+ deriveWalletLifetimeDeltas,
1695
2964
  economyAssert,
1696
2965
  evaluateEarlyBacker,
2966
+ evaluateEarlyBackerBySettlementV2,
1697
2967
  isWholeTokenAmount,
1698
2968
  mapBitLabsConversionState,
1699
2969
  parseIsoTimestamp,