@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.js CHANGED
@@ -37,7 +37,7 @@ function assertInRange(amount) {
37
37
  }
38
38
  function parseTokenSubunits(value) {
39
39
  economyAssert(
40
- CANONICAL_INTEGER.test(value),
40
+ typeof value === "string" && CANONICAL_INTEGER.test(value),
41
41
  "INVALID_AMOUNT",
42
42
  "TokenSubunit amount must be a canonical base-10 integer string"
43
43
  );
@@ -86,12 +86,17 @@ var ECONOMY_CONTRACT_VERSION = "1";
86
86
  var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
87
87
  function assertEconomyIdentifier(value, label = "identifier") {
88
88
  economyAssert(
89
- IDENTIFIER.test(value),
89
+ typeof value === "string" && IDENTIFIER.test(value),
90
90
  "INVALID_CONTRACT",
91
91
  `${label} must be a non-empty opaque identifier`
92
92
  );
93
93
  }
94
94
  function parseIsoTimestamp(value) {
95
+ economyAssert(
96
+ typeof value === "string",
97
+ "INVALID_CONTRACT",
98
+ "Timestamp must be an ISO-8601 UTC value"
99
+ );
95
100
  const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{3}))?Z$/u.exec(
96
101
  value
97
102
  );
@@ -716,6 +721,7 @@ function reclaimGameplayAllocation(allocation, input) {
716
721
 
717
722
  // src/early-backers.ts
718
723
  var PRE_UTILITY_BACKER_COHORT = "pre_utility_backer_v1";
724
+ var EARLY_BACKER_SETTLEMENT_POLICY_V2 = "settlement-window-v2";
719
725
  function isWithinWindow(timestamp, launch, cutoff) {
720
726
  const value = parseIsoTimestamp(timestamp);
721
727
  return value >= launch && (cutoff === void 0 || value < cutoff);
@@ -787,6 +793,71 @@ function evaluateEarlyBacker(payerAccountId, receivingHouseholdId, paidLots, win
787
793
  evaluatedAt
788
794
  };
789
795
  }
796
+ function evaluateEarlyBackerBySettlementV2(payerAccountId, receivingHouseholdId, paidLots, window, evaluatedAt) {
797
+ assertEconomyIdentifier(payerAccountId, "payerAccountId");
798
+ assertEconomyIdentifier(receivingHouseholdId, "receivingHouseholdId");
799
+ const launch = parseIsoTimestamp(window.publicTokensLaunchAt);
800
+ const cutoff = window.firstPublicSpendLiveAt === void 0 ? void 0 : parseIsoTimestamp(window.firstPublicSpendLiveAt);
801
+ economyAssert(
802
+ cutoff === void 0 || cutoff > launch,
803
+ "INVALID_TIME_WINDOW",
804
+ "First public spend must occur after public Token launch"
805
+ );
806
+ const evaluationTime = parseIsoTimestamp(evaluatedAt);
807
+ let retained = 0n;
808
+ const contributingLotIds = [];
809
+ const seenLotIds = /* @__PURE__ */ new Set();
810
+ for (const lot of paidLots) {
811
+ economyAssert(
812
+ lot.schemaVersion === ECONOMY_CONTRACT_VERSION,
813
+ "INVALID_CONTRACT",
814
+ "Unsupported paid-lot retention contract version"
815
+ );
816
+ assertEconomyIdentifier(lot.lotId, "lotId");
817
+ economyAssert(
818
+ !seenLotIds.has(lot.lotId),
819
+ "DUPLICATE_IDENTIFIER",
820
+ "Paid-lot retention input must not repeat a source lot"
821
+ );
822
+ seenLotIds.add(lot.lotId);
823
+ assertEconomyIdentifier(lot.payerAccountId, "payerAccountId");
824
+ assertEconomyIdentifier(lot.receivingHouseholdId, "receivingHouseholdId");
825
+ assertEconomyIdentifier(lot.purchaseId, "purchaseId");
826
+ assertEconomyIdentifier(lot.catalogVersion, "catalogVersion");
827
+ const purchasedAt = parseIsoTimestamp(lot.purchasedAt);
828
+ const settledAt = parseIsoTimestamp(lot.settledAt);
829
+ const creditedAt = parseIsoTimestamp(lot.creditedAt);
830
+ economyAssert(
831
+ purchasedAt <= settledAt && settledAt <= creditedAt,
832
+ "INVALID_TIME_WINDOW",
833
+ "Paid-lot timestamps must follow purchase, settlement, then credit order"
834
+ );
835
+ const amount = parseTokenSubunits(lot.retainedAmount);
836
+ economyAssert(
837
+ amount >= 0n,
838
+ "INVALID_AMOUNT",
839
+ "Retained paid-lot basis cannot be negative"
840
+ );
841
+ if (lot.payerAccountId === payerAccountId && lot.receivingHouseholdId === receivingHouseholdId && settledAt >= launch && (cutoff === void 0 || settledAt < cutoff) && settledAt <= evaluationTime && amount > 0n) {
842
+ retained += amount;
843
+ contributingLotIds.push(lot.lotId);
844
+ }
845
+ }
846
+ return {
847
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
848
+ policyVersion: EARLY_BACKER_SETTLEMENT_POLICY_V2,
849
+ qualificationEvent: "settled-at",
850
+ cohortKey: PRE_UTILITY_BACKER_COHORT,
851
+ status: retained > 0n ? "provisional" : "not-qualified",
852
+ payerAccountId,
853
+ receivingHouseholdId,
854
+ netRetainedAmount: serializeTokenSubunits(retained),
855
+ contributingLotIds: contributingLotIds.sort(
856
+ (left, right) => left.localeCompare(right)
857
+ ),
858
+ evaluatedAt
859
+ };
860
+ }
790
861
 
791
862
  // src/ledger.ts
792
863
  var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
@@ -813,6 +884,15 @@ var ACTIVITY_STATUSES = /* @__PURE__ */ new Set([
813
884
  "reversed",
814
885
  "failed"
815
886
  ]);
887
+ var ACTIVITY_SOURCES = /* @__PURE__ */ new Set([
888
+ "shopify",
889
+ "ayet",
890
+ "bitlabs",
891
+ "subscription",
892
+ "event",
893
+ "competition",
894
+ "adjustment"
895
+ ]);
816
896
  function assertCanonicalHash(value, label) {
817
897
  economyAssert(
818
898
  /^sha256:[a-f0-9]{64}$/u.test(value),
@@ -834,9 +914,9 @@ function assertActivityEntry(entry) {
834
914
  );
835
915
  assertEconomyIdentifier(entry.transactionId, "transactionId");
836
916
  economyAssert(
837
- ACTIVITY_TYPES.has(entry.activityType) && ACTIVITY_STATUSES.has(entry.status),
917
+ ACTIVITY_TYPES.has(entry.activityType) && ACTIVITY_STATUSES.has(entry.status) && ACTIVITY_SOURCES.has(entry.source),
838
918
  "INVALID_CONTRACT",
839
- "Activity type or status is unsupported"
919
+ "Activity type, status, or source is unsupported"
840
920
  );
841
921
  parseIsoTimestamp(entry.occurredAt);
842
922
  economyAssert(
@@ -852,13 +932,13 @@ function assertActivityEntry(entry) {
852
932
  }
853
933
  if (entry.maskedReference !== void 0) {
854
934
  economyAssert(
855
- entry.maskedReference.length > 0 && entry.maskedReference.length <= 128 && !hasControlCharacters(entry.maskedReference),
935
+ typeof entry.maskedReference === "string" && entry.maskedReference.length > 0 && entry.maskedReference.length <= 128 && !hasControlCharacters(entry.maskedReference),
856
936
  "INVALID_CONTRACT",
857
937
  "Masked activity references must be bounded and safe for display"
858
938
  );
859
939
  }
860
940
  economyAssert(
861
- entry.sourceLabel.trim().length > 0 && entry.sourceLabel.length <= 128 && !hasControlCharacters(entry.sourceLabel),
941
+ typeof entry.sourceLabel === "string" && entry.sourceLabel.trim().length > 0 && entry.sourceLabel.length <= 128 && !hasControlCharacters(entry.sourceLabel),
862
942
  "INVALID_CONTRACT",
863
943
  "Activity source labels must be bounded and safe for display"
864
944
  );
@@ -957,6 +1037,14 @@ function assertBalancedTransaction(transaction) {
957
1037
  "Ledger postings must sum to zero TokenSubunits"
958
1038
  );
959
1039
  }
1040
+ function assertEconomicJournalTransaction(transaction) {
1041
+ assertBalancedTransaction(transaction);
1042
+ economyAssert(
1043
+ transaction.status === "held" || transaction.status === "settled",
1044
+ "INVALID_CONTRACT",
1045
+ "Pending, failed, and derived reversed activity cannot be journal transactions"
1046
+ );
1047
+ }
960
1048
  function canonicalTransactionPayload(transaction) {
961
1049
  assertBalancedTransaction(transaction);
962
1050
  const postings = [...transaction.postings].sort((left, right) => left.postingId.localeCompare(right.postingId)).map((posting) => ({
@@ -1036,6 +1124,71 @@ function assertReversalAvailable(originalTransactionId, existingTransactions) {
1036
1124
  );
1037
1125
  }
1038
1126
 
1127
+ // src/integrity.ts
1128
+ function assertCanonicalHash2(value, label) {
1129
+ economyAssert(
1130
+ typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value),
1131
+ "INVALID_CONTRACT",
1132
+ `${label} must be a canonical SHA-256 reference`
1133
+ );
1134
+ }
1135
+ function assertJournalChainHead(head) {
1136
+ economyAssert(
1137
+ head.schemaVersion === ECONOMY_CONTRACT_VERSION,
1138
+ "INVALID_CONTRACT",
1139
+ "Unsupported journal-chain-head contract version"
1140
+ );
1141
+ assertEconomyIdentifier(head.chainId, "chainId");
1142
+ economyAssert(
1143
+ Number.isSafeInteger(head.version) && head.version >= 0,
1144
+ "INVALID_CONTRACT",
1145
+ "Journal-chain-head version must be a non-negative safe integer"
1146
+ );
1147
+ parseIsoTimestamp(head.updatedAt);
1148
+ const hasTransaction = head.lastTransactionId !== void 0;
1149
+ const hasHash = head.canonicalHash !== void 0;
1150
+ economyAssert(
1151
+ hasTransaction === hasHash && head.version > 0 === hasTransaction,
1152
+ "INVALID_CONTRACT",
1153
+ "Journal-chain head identity, hash, and version must describe the same state"
1154
+ );
1155
+ if (head.lastTransactionId !== void 0) {
1156
+ assertEconomyIdentifier(head.lastTransactionId, "lastTransactionId");
1157
+ }
1158
+ if (head.canonicalHash !== void 0) {
1159
+ assertCanonicalHash2(head.canonicalHash, "Journal-chain-head hash");
1160
+ }
1161
+ }
1162
+ function advanceJournalChainHead(head, transaction) {
1163
+ assertJournalChainHead(head);
1164
+ assertEconomicJournalTransaction(transaction);
1165
+ assertCanonicalHash2(transaction.canonicalHash, "Transaction hash");
1166
+ economyAssert(
1167
+ transaction.previousCanonicalHash === head.canonicalHash,
1168
+ "INVALID_CONTRACT",
1169
+ "Transaction previous hash must match the locked journal-chain head"
1170
+ );
1171
+ economyAssert(
1172
+ transaction.canonicalHash !== head.canonicalHash,
1173
+ "DUPLICATE_TRANSACTION",
1174
+ "Transaction hash must advance the journal chain"
1175
+ );
1176
+ const recordedAt = parseIsoTimestamp(transaction.recordedAt);
1177
+ economyAssert(
1178
+ recordedAt >= parseIsoTimestamp(head.updatedAt),
1179
+ "INVALID_TIME_WINDOW",
1180
+ "Journal-chain update cannot precede its locked head"
1181
+ );
1182
+ return {
1183
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1184
+ chainId: head.chainId,
1185
+ version: head.version + 1,
1186
+ lastTransactionId: transaction.transactionId,
1187
+ canonicalHash: transaction.canonicalHash,
1188
+ updatedAt: transaction.recordedAt
1189
+ };
1190
+ }
1191
+
1039
1192
  // src/lots.ts
1040
1193
  function assertSourceLot(lot) {
1041
1194
  economyAssert(
@@ -1132,6 +1285,199 @@ function assertSourceLot(lot) {
1132
1285
  "Remaining and reversed source-lot amounts exceed the original"
1133
1286
  );
1134
1287
  }
1288
+ function assertVersionedSourceLot(snapshot) {
1289
+ economyAssert(
1290
+ snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
1291
+ "INVALID_CONTRACT",
1292
+ "Unsupported versioned source-lot contract version"
1293
+ );
1294
+ assertSourceLot(snapshot.lot);
1295
+ economyAssert(
1296
+ Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
1297
+ "INVALID_CONTRACT",
1298
+ "Source-lot version must be a positive safe integer"
1299
+ );
1300
+ economyAssert(
1301
+ parseIsoTimestamp(snapshot.updatedAt) >= parseIsoTimestamp(snapshot.lot.creditedAt),
1302
+ "INVALID_TIME_WINDOW",
1303
+ "Source-lot update time cannot precede its credit"
1304
+ );
1305
+ }
1306
+ function createInitialSourceLotSnapshot(lot) {
1307
+ assertSourceLot(lot);
1308
+ economyAssert(
1309
+ lot.refundState === "none" && lot.originalAmount === lot.remainingAmount && parseTokenSubunits(lot.heldAmount) === 0n && parseTokenSubunits(lot.reversedAmount) === 0n,
1310
+ "INVALID_CONTRACT",
1311
+ "A new source lot must start fully remaining, clear, and unreversed"
1312
+ );
1313
+ return {
1314
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1315
+ lot,
1316
+ version: 1,
1317
+ updatedAt: lot.creditedAt
1318
+ };
1319
+ }
1320
+ function assertRefundState(value) {
1321
+ economyAssert(
1322
+ [
1323
+ "none",
1324
+ "partial",
1325
+ "refunded",
1326
+ "disputed",
1327
+ "chargeback-lost"
1328
+ ].includes(value),
1329
+ "INVALID_CONTRACT",
1330
+ "Source-lot movement has an unsupported refund state"
1331
+ );
1332
+ }
1333
+ function assertSourceLotMovement(movement) {
1334
+ economyAssert(
1335
+ movement.schemaVersion === ECONOMY_CONTRACT_VERSION,
1336
+ "INVALID_CONTRACT",
1337
+ "Unsupported source-lot-movement contract version"
1338
+ );
1339
+ assertEconomyIdentifier(movement.movementId, "movementId");
1340
+ assertEconomyIdentifier(movement.transactionId, "transactionId");
1341
+ assertEconomyIdentifier(movement.lotId, "lotId");
1342
+ economyAssert(
1343
+ [
1344
+ "allocate",
1345
+ "boost",
1346
+ "reclaim",
1347
+ "spend",
1348
+ "hold",
1349
+ "release-hold",
1350
+ "refund",
1351
+ "chargeback",
1352
+ "reversal"
1353
+ ].includes(movement.movementType),
1354
+ "INVALID_CONTRACT",
1355
+ "Source-lot movement has an unsupported type"
1356
+ );
1357
+ const remaining = parseTokenSubunits(movement.remainingDelta);
1358
+ const held = parseTokenSubunits(movement.heldDelta);
1359
+ const reversed = parseTokenSubunits(movement.reversedDelta);
1360
+ economyAssert(
1361
+ remaining !== 0n || held !== 0n || reversed !== 0n,
1362
+ "INVALID_AMOUNT",
1363
+ "Source-lot movement must change at least one amount"
1364
+ );
1365
+ economyAssert(
1366
+ Number.isSafeInteger(movement.expectedVersion) && movement.expectedVersion >= 1 && movement.resultingVersion === movement.expectedVersion + 1,
1367
+ "INVALID_CONTRACT",
1368
+ "Source-lot movement must advance its optimistic version exactly once"
1369
+ );
1370
+ assertRefundState(movement.expectedRefundState);
1371
+ assertRefundState(movement.resultingRefundState);
1372
+ parseIsoTimestamp(movement.occurredAt);
1373
+ if (movement.movementType === "allocate" || movement.movementType === "boost" || movement.movementType === "spend") {
1374
+ economyAssert(
1375
+ remaining < 0n && held === 0n && reversed === 0n && movement.resultingRefundState === movement.expectedRefundState,
1376
+ "INVALID_CONTRACT",
1377
+ "Allocation, boost, and spend movements only consume remaining lot value"
1378
+ );
1379
+ } else if (movement.movementType === "reclaim") {
1380
+ economyAssert(
1381
+ remaining > 0n && held === 0n && reversed === 0n && movement.resultingRefundState === movement.expectedRefundState,
1382
+ "INVALID_CONTRACT",
1383
+ "Reclaim movements only restore remaining lot value"
1384
+ );
1385
+ } else if (movement.movementType === "hold") {
1386
+ economyAssert(
1387
+ remaining === 0n && held > 0n && reversed === 0n && movement.resultingRefundState === "disputed",
1388
+ "INVALID_CONTRACT",
1389
+ "Hold movements must enter disputed state and increase held value"
1390
+ );
1391
+ } else if (movement.movementType === "release-hold") {
1392
+ economyAssert(
1393
+ remaining === 0n && held < 0n && reversed === 0n && movement.expectedRefundState === "disputed" && movement.resultingRefundState !== "disputed" && movement.resultingRefundState !== "refunded" && movement.resultingRefundState !== "chargeback-lost",
1394
+ "INVALID_CONTRACT",
1395
+ "Released holds must leave disputed state without reversing value"
1396
+ );
1397
+ } else {
1398
+ economyAssert(
1399
+ remaining < 0n && reversed === -remaining && held <= 0n && (movement.resultingRefundState === "partial" || movement.resultingRefundState === "refunded" || movement.resultingRefundState === "chargeback-lost"),
1400
+ "INVALID_CONTRACT",
1401
+ "Refund, chargeback, and reversal movements must compensate remaining value exactly"
1402
+ );
1403
+ if (movement.movementType === "chargeback") {
1404
+ economyAssert(
1405
+ movement.resultingRefundState === "chargeback-lost",
1406
+ "INVALID_CONTRACT",
1407
+ "Chargeback movements must enter chargeback-lost state"
1408
+ );
1409
+ } else {
1410
+ economyAssert(
1411
+ movement.resultingRefundState !== "chargeback-lost",
1412
+ "INVALID_CONTRACT",
1413
+ "Only a chargeback movement may enter chargeback-lost state"
1414
+ );
1415
+ }
1416
+ }
1417
+ }
1418
+ function applySourceLotMovement(snapshot, movement) {
1419
+ assertVersionedSourceLot(snapshot);
1420
+ assertSourceLotMovement(movement);
1421
+ economyAssert(
1422
+ movement.lotId === snapshot.lot.lotId && movement.expectedVersion === snapshot.version && movement.expectedRefundState === snapshot.lot.refundState,
1423
+ "INVALID_CONTRACT",
1424
+ "Source-lot movement does not match the locked lot version and refund state"
1425
+ );
1426
+ economyAssert(
1427
+ parseIsoTimestamp(movement.occurredAt) >= parseIsoTimestamp(snapshot.updatedAt),
1428
+ "INVALID_TIME_WINDOW",
1429
+ "Source-lot movement cannot precede its current projection"
1430
+ );
1431
+ const lot = {
1432
+ ...snapshot.lot,
1433
+ remainingAmount: serializeTokenSubunits(
1434
+ parseTokenSubunits(snapshot.lot.remainingAmount) + parseTokenSubunits(movement.remainingDelta)
1435
+ ),
1436
+ heldAmount: serializeTokenSubunits(
1437
+ parseTokenSubunits(snapshot.lot.heldAmount) + parseTokenSubunits(movement.heldDelta)
1438
+ ),
1439
+ reversedAmount: serializeTokenSubunits(
1440
+ parseTokenSubunits(snapshot.lot.reversedAmount) + parseTokenSubunits(movement.reversedDelta)
1441
+ ),
1442
+ refundState: movement.resultingRefundState
1443
+ };
1444
+ assertSourceLot(lot);
1445
+ const remaining = parseTokenSubunits(lot.remainingAmount);
1446
+ const held = parseTokenSubunits(lot.heldAmount);
1447
+ const reversed = parseTokenSubunits(lot.reversedAmount);
1448
+ const original = parseTokenSubunits(lot.originalAmount);
1449
+ if (lot.refundState === "none") {
1450
+ economyAssert(
1451
+ reversed === 0n && held === 0n,
1452
+ "INVALID_CONTRACT",
1453
+ "A clear lot cannot retain held or reversed value"
1454
+ );
1455
+ } else if (lot.refundState === "partial") {
1456
+ economyAssert(
1457
+ reversed > 0n && remaining > 0n && held === 0n,
1458
+ "INVALID_CONTRACT",
1459
+ "A partial refund requires retained/reversed value and no active hold"
1460
+ );
1461
+ } else if (lot.refundState === "disputed") {
1462
+ economyAssert(
1463
+ held > 0n,
1464
+ "INVALID_CONTRACT",
1465
+ "A disputed lot must hold affected value"
1466
+ );
1467
+ } else {
1468
+ economyAssert(
1469
+ remaining === 0n && held === 0n && reversed === original,
1470
+ "INVALID_CONTRACT",
1471
+ "Final refund and lost-chargeback states must reverse the full lot"
1472
+ );
1473
+ }
1474
+ return {
1475
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1476
+ lot,
1477
+ version: movement.resultingVersion,
1478
+ updatedAt: movement.occurredAt
1479
+ };
1480
+ }
1135
1481
  function availableSourceLotAmount(lot) {
1136
1482
  assertSourceLot(lot);
1137
1483
  return parseTokenSubunits(lot.remainingAmount) - parseTokenSubunits(lot.heldAmount);
@@ -1234,193 +1580,137 @@ function assertDistinctAdjustmentApproval(request) {
1234
1580
  }
1235
1581
  }
1236
1582
 
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;
1583
+ // src/ports/persistence.ts
1584
+ function assertScopedPersistedIdempotencyResult(result) {
1585
+ economyAssert(
1586
+ result.schemaVersion === ECONOMY_CONTRACT_VERSION,
1587
+ "INVALID_CONTRACT",
1588
+ "Unsupported scoped idempotency-result contract version"
1589
+ );
1590
+ assertEconomyIdentifier(result.commandId, "commandId");
1591
+ assertEconomyIdentifier(result.transactionId, "transactionId");
1592
+ economyAssert(
1593
+ /^sha256:[a-f0-9]{64}$/u.test(result.responseHash),
1594
+ "INVALID_CONTRACT",
1595
+ "Idempotency response hash must be a canonical SHA-256 reference"
1596
+ );
1597
+ parseIsoTimestamp(result.recordedAt);
1248
1598
  }
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
- );
1599
+ var ECONOMY_COMMAND_TYPES = /* @__PURE__ */ new Set([
1600
+ "credit-purchase",
1601
+ "credit-subscription",
1602
+ "credit-reward",
1603
+ "credit-event",
1604
+ "credit-competition",
1605
+ "allocate",
1606
+ "boost",
1607
+ "reclaim",
1608
+ "spend",
1609
+ "hold",
1610
+ "release-hold",
1611
+ "refund",
1612
+ "chargeback",
1613
+ "reverse",
1614
+ "adjust"
1615
+ ]);
1616
+ function assertEconomyIdempotencyScope(scope) {
1617
+ economyAssert(
1618
+ scope.schemaVersion === ECONOMY_CONTRACT_VERSION && ECONOMY_COMMAND_TYPES.has(scope.commandType),
1619
+ "INVALID_CONTRACT",
1620
+ "Unsupported economy idempotency scope"
1621
+ );
1622
+ assertEconomyIdentifier(scope.idempotencyKey, "idempotencyKey");
1623
+ assertEconomyIdentifier(scope.actorAccountId, "actorAccountId");
1624
+ assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
1625
+ }
1626
+ function assertEconomyCommandEnvelope(envelope) {
1627
+ economyAssert(
1628
+ envelope.schemaVersion === ECONOMY_CONTRACT_VERSION,
1629
+ "INVALID_CONTRACT",
1630
+ "Unsupported economy-command-envelope contract version"
1631
+ );
1632
+ assertEconomyIdentifier(envelope.commandId, "commandId");
1633
+ economyAssert(
1634
+ ECONOMY_COMMAND_TYPES.has(envelope.commandType),
1635
+ "INVALID_CONTRACT",
1636
+ "Economy command has an unsupported type"
1637
+ );
1638
+ assertEconomyIdentifier(envelope.idempotencyKey, "idempotencyKey");
1639
+ assertEconomyIdentifier(envelope.actorAccountId, "actorAccountId");
1640
+ assertEconomyIdentifier(envelope.subjectAccountId, "subjectAccountId");
1641
+ if (envelope.relationshipId !== void 0) {
1642
+ assertEconomyIdentifier(envelope.relationshipId, "relationshipId");
1643
+ }
1644
+ if (envelope.authorizationVersion !== void 0) {
1260
1645
  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"
1646
+ Number.isSafeInteger(envelope.authorizationVersion) && envelope.authorizationVersion >= 1,
1647
+ "INVALID_CONTRACT",
1648
+ "Command authorization version must be a positive safe integer"
1285
1649
  );
1286
1650
  }
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
1651
  economyAssert(
1313
- plan.schemaVersion === ECONOMY_CONTRACT_VERSION,
1652
+ envelope.relationshipId === void 0 === (envelope.authorizationVersion === void 0),
1314
1653
  "INVALID_CONTRACT",
1315
- "Unsupported subscription-plan contract version"
1654
+ "Relationship identity and authorization version must be supplied together"
1316
1655
  );
1317
- assertEconomyIdentifier(plan.planId, "planId");
1318
- assertEconomyIdentifier(plan.catalogVersion, "catalogVersion");
1319
1656
  economyAssert(
1320
- ["daily", "weekly", "monthly"].includes(plan.interval) && plan.currency === "GBP" && typeof plan.enabled === "boolean",
1657
+ /^sha256:[a-f0-9]{64}$/u.test(envelope.payloadHash),
1321
1658
  "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"
1659
+ "Command payload hash must be a canonical SHA-256 reference"
1328
1660
  );
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"
1661
+ parseIsoTimestamp(envelope.acceptedAt);
1662
+ assertEconomyIdentifier(envelope.acceptedRegion, "acceptedRegion");
1663
+ assertEconomyIdentifier(
1664
+ envelope.writerFencingToken,
1665
+ "writerFencingToken"
1334
1666
  );
1335
- parseIsoTimestamp(plan.effectiveFrom);
1336
1667
  }
1337
-
1338
- // src/spend-requests.ts
1339
- function assertSpendRequest(request) {
1668
+ function assertEconomyCommandWorkflowEvent(event) {
1340
1669
  economyAssert(
1341
- request.schemaVersion === ECONOMY_CONTRACT_VERSION,
1670
+ event.schemaVersion === ECONOMY_CONTRACT_VERSION,
1342
1671
  "INVALID_CONTRACT",
1343
- "Unsupported spend-request contract version"
1672
+ "Unsupported command-workflow-event contract version"
1344
1673
  );
1345
- assertEconomyIdentifier(request.requestId, "requestId");
1346
- assertEconomyIdentifier(request.childAccountId, "childAccountId");
1347
- assertEconomyIdentifier(request.allocationId, "allocationId");
1674
+ assertEconomyIdentifier(event.workflowEventId, "workflowEventId");
1675
+ assertEconomyIdentifier(event.commandId, "commandId");
1348
1676
  economyAssert(
1349
- request.purpose === "gameplay-conversion" || request.purpose === "non-gameplay",
1677
+ ["accepted", "processing", "failed", "completed"].includes(event.state),
1350
1678
  "INVALID_CONTRACT",
1351
- "Spend request has an unsupported purpose"
1679
+ "Command workflow has an unsupported state"
1352
1680
  );
1353
1681
  economyAssert(
1354
- [
1355
- "requested",
1356
- "approved",
1357
- "rejected",
1358
- "expired",
1359
- "executed",
1360
- "cancelled"
1361
- ].includes(request.status),
1682
+ event.state === "failed" === (event.failureCode !== void 0),
1362
1683
  "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
1684
+ "Only failed command workflows require a failure code"
1379
1685
  );
1686
+ if (event.failureCode !== void 0) {
1687
+ assertEconomyIdentifier(event.failureCode, "failureCode");
1688
+ }
1689
+ parseIsoTimestamp(event.occurredAt);
1690
+ }
1691
+
1692
+ // src/wallets.ts
1693
+ function assertWalletOwnerReference(owner) {
1380
1694
  economyAssert(
1381
- !decisionRequired || request.decidedByAccountId !== void 0 && request.decidedAt !== void 0,
1695
+ ["account", "household", "system"].includes(owner.ownerType),
1382
1696
  "INVALID_CONTRACT",
1383
- "Decided spend requests require a guardian actor and timestamp"
1697
+ "Wallet owner reference has an unsupported type"
1384
1698
  );
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") {
1699
+ if (owner.ownerType === "system") {
1404
1700
  economyAssert(
1405
- request.approvalNonceHash !== void 0,
1701
+ owner.ownerId === "system",
1406
1702
  "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"
1703
+ "System wallet lookups must use the system owner"
1414
1704
  );
1705
+ } else {
1706
+ assertEconomyIdentifier(owner.ownerId, "ownerId");
1415
1707
  economyAssert(
1416
- request.status === "executed",
1708
+ owner.ownerId !== "system",
1417
1709
  "INVALID_CONTRACT",
1418
- "Only executed spend requests may reference a ledger transaction"
1710
+ "Account and household wallets cannot use the system owner"
1419
1711
  );
1420
1712
  }
1421
1713
  }
1422
-
1423
- // src/wallets.ts
1424
1714
  function assertWallet(wallet) {
1425
1715
  economyAssert(
1426
1716
  wallet.schemaVersion === ECONOMY_CONTRACT_VERSION,
@@ -1566,26 +1856,951 @@ function assertWalletLifetimeTotals(totals) {
1566
1856
  );
1567
1857
  }
1568
1858
  }
1859
+
1860
+ // src/projection.ts
1861
+ function assertWalletBalanceProjection(projection) {
1862
+ economyAssert(
1863
+ projection.schemaVersion === ECONOMY_CONTRACT_VERSION,
1864
+ "INVALID_CONTRACT",
1865
+ "Unsupported wallet-balance-projection contract version"
1866
+ );
1867
+ assertEconomyIdentifier(projection.walletId, "walletId");
1868
+ for (const amount of [
1869
+ projection.spendable,
1870
+ projection.reserved,
1871
+ projection.held
1872
+ ]) {
1873
+ economyAssert(
1874
+ parseTokenSubunits(amount) >= 0n,
1875
+ "NEGATIVE_PROJECTION",
1876
+ "Wallet projection buckets cannot be negative"
1877
+ );
1878
+ }
1879
+ economyAssert(
1880
+ Number.isSafeInteger(projection.version) && projection.version >= 1,
1881
+ "INVALID_CONTRACT",
1882
+ "Wallet projection version must be a positive safe integer"
1883
+ );
1884
+ if (projection.lastTransactionId !== void 0) {
1885
+ assertEconomyIdentifier(
1886
+ projection.lastTransactionId,
1887
+ "lastTransactionId"
1888
+ );
1889
+ }
1890
+ parseIsoTimestamp(projection.asOf);
1891
+ }
1892
+ function assertWalletBalanceDelta(delta) {
1893
+ economyAssert(
1894
+ delta.schemaVersion === ECONOMY_CONTRACT_VERSION,
1895
+ "INVALID_CONTRACT",
1896
+ "Unsupported wallet-balance-delta contract version"
1897
+ );
1898
+ assertEconomyIdentifier(delta.walletId, "walletId");
1899
+ const values = [
1900
+ parseTokenSubunits(delta.spendableDelta),
1901
+ parseTokenSubunits(delta.reservedDelta),
1902
+ parseTokenSubunits(delta.heldDelta)
1903
+ ];
1904
+ economyAssert(
1905
+ values.some((value) => value !== 0n),
1906
+ "INVALID_AMOUNT",
1907
+ "Wallet balance delta must change at least one bucket"
1908
+ );
1909
+ }
1910
+ function assertWalletBalanceDeltaBatch(transactionId, deltas) {
1911
+ assertEconomyIdentifier(transactionId, "transactionId");
1912
+ economyAssert(
1913
+ deltas.length > 0,
1914
+ "INVALID_CONTRACT",
1915
+ "An economic transaction must update at least one wallet projection"
1916
+ );
1917
+ const walletIds = /* @__PURE__ */ new Set();
1918
+ for (const delta of deltas) {
1919
+ assertWalletBalanceDelta(delta);
1920
+ economyAssert(
1921
+ !walletIds.has(delta.walletId),
1922
+ "DUPLICATE_IDENTIFIER",
1923
+ "A balance-delta batch must update each wallet once"
1924
+ );
1925
+ walletIds.add(delta.walletId);
1926
+ }
1927
+ }
1928
+ function applyWalletBalanceDelta(projection, delta, transactionId, asOf) {
1929
+ assertWalletBalanceProjection(projection);
1930
+ assertWalletBalanceDelta(delta);
1931
+ assertEconomyIdentifier(transactionId, "transactionId");
1932
+ economyAssert(
1933
+ projection.walletId === delta.walletId,
1934
+ "INVALID_CONTRACT",
1935
+ "Wallet balance delta must target its projection wallet"
1936
+ );
1937
+ economyAssert(
1938
+ parseIsoTimestamp(asOf) >= parseIsoTimestamp(projection.asOf),
1939
+ "INVALID_TIME_WINDOW",
1940
+ "Wallet balance update cannot precede its projection"
1941
+ );
1942
+ const next = {
1943
+ ...projection,
1944
+ spendable: serializeTokenSubunits(
1945
+ parseTokenSubunits(projection.spendable) + parseTokenSubunits(delta.spendableDelta)
1946
+ ),
1947
+ reserved: serializeTokenSubunits(
1948
+ parseTokenSubunits(projection.reserved) + parseTokenSubunits(delta.reservedDelta)
1949
+ ),
1950
+ held: serializeTokenSubunits(
1951
+ parseTokenSubunits(projection.held) + parseTokenSubunits(delta.heldDelta)
1952
+ ),
1953
+ version: projection.version + 1,
1954
+ lastTransactionId: transactionId,
1955
+ asOf
1956
+ };
1957
+ assertWalletBalanceProjection(next);
1958
+ return next;
1959
+ }
1960
+ function createWalletBalanceSummary(projection) {
1961
+ assertWalletBalanceProjection(projection);
1962
+ const spendable = parseTokenSubunits(projection.spendable);
1963
+ const rewardProgress = spendable % 1000n;
1964
+ const summary = {
1965
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1966
+ walletId: projection.walletId,
1967
+ available: serializeTokenSubunits(spendable - rewardProgress),
1968
+ reserved: projection.reserved,
1969
+ held: projection.held,
1970
+ rewardProgress: serializeTokenSubunits(rewardProgress),
1971
+ version: projection.version,
1972
+ asOf: projection.asOf
1973
+ };
1974
+ assertDeterministicWalletBalanceSummary(summary);
1975
+ return summary;
1976
+ }
1977
+ function assertDeterministicWalletBalanceSummary(summary) {
1978
+ assertWalletBalanceSummary(summary);
1979
+ economyAssert(
1980
+ parseTokenSubunits(summary.available) % 1000n === 0n && parseTokenSubunits(summary.rewardProgress) < 1000n,
1981
+ "INVALID_CONTRACT",
1982
+ "Per-wallet availability must be whole Tokens with sub-Token progress below one Token"
1983
+ );
1984
+ }
1985
+ function deriveWalletBalanceDeltas(transaction, bindings) {
1986
+ assertEconomicJournalTransaction(transaction);
1987
+ const byAccount = /* @__PURE__ */ new Map();
1988
+ for (const binding of bindings) {
1989
+ assertEconomyIdentifier(binding.accountId, "accountId");
1990
+ assertEconomyIdentifier(binding.walletId, "walletId");
1991
+ economyAssert(
1992
+ ["spendable", "reserved", "held"].includes(binding.bucket),
1993
+ "INVALID_CONTRACT",
1994
+ "Wallet account binding has an unsupported bucket"
1995
+ );
1996
+ economyAssert(
1997
+ !byAccount.has(binding.accountId),
1998
+ "DUPLICATE_IDENTIFIER",
1999
+ "A ledger account may bind to only one wallet bucket"
2000
+ );
2001
+ byAccount.set(binding.accountId, binding);
2002
+ }
2003
+ const byWallet = /* @__PURE__ */ new Map();
2004
+ for (const posting of transaction.postings) {
2005
+ if (posting.walletId === void 0) {
2006
+ continue;
2007
+ }
2008
+ const binding = byAccount.get(posting.accountId);
2009
+ economyAssert(
2010
+ binding !== void 0 && binding.walletId === posting.walletId,
2011
+ "INVALID_CONTRACT",
2012
+ "Every wallet-linked posting must match an authoritative account binding"
2013
+ );
2014
+ const values = byWallet.get(binding.walletId) ?? [0n, 0n, 0n];
2015
+ const amount = parseTokenSubunits(posting.amount);
2016
+ if (binding.bucket === "spendable") {
2017
+ values[0] += amount;
2018
+ } else if (binding.bucket === "reserved") {
2019
+ values[1] += amount;
2020
+ } else {
2021
+ values[2] += amount;
2022
+ }
2023
+ byWallet.set(binding.walletId, values);
2024
+ }
2025
+ const deltas = [...byWallet.entries()].map(([walletId, values]) => ({
2026
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2027
+ walletId,
2028
+ spendableDelta: serializeTokenSubunits(values[0]),
2029
+ reservedDelta: serializeTokenSubunits(values[1]),
2030
+ heldDelta: serializeTokenSubunits(values[2])
2031
+ })).filter(
2032
+ (delta) => delta.spendableDelta !== "0" || delta.reservedDelta !== "0" || delta.heldDelta !== "0"
2033
+ ).sort((left, right) => left.walletId.localeCompare(right.walletId));
2034
+ assertWalletBalanceDeltaBatch(transaction.transactionId, deltas);
2035
+ return deltas;
2036
+ }
2037
+ var EARNED_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
2038
+ "rewarded-ad",
2039
+ "offerwall",
2040
+ "event",
2041
+ "competition"
2042
+ ]);
2043
+ function deriveWalletLifetimeDeltas(transaction) {
2044
+ assertEconomicJournalTransaction(transaction);
2045
+ if (transaction.status !== "settled") {
2046
+ return [];
2047
+ }
2048
+ const netByWallet = /* @__PURE__ */ new Map();
2049
+ for (const posting of transaction.postings) {
2050
+ if (posting.walletId !== void 0) {
2051
+ netByWallet.set(
2052
+ posting.walletId,
2053
+ (netByWallet.get(posting.walletId) ?? 0n) + parseTokenSubunits(posting.amount)
2054
+ );
2055
+ }
2056
+ }
2057
+ const deltas = [];
2058
+ for (const [walletId, net] of [...netByWallet.entries()].sort(
2059
+ ([left], [right]) => left.localeCompare(right)
2060
+ )) {
2061
+ let bucket;
2062
+ let amount = 0n;
2063
+ if (net > 0n && (transaction.activityType === "purchase" || transaction.activityType === "subscription")) {
2064
+ bucket = "bought";
2065
+ amount = net;
2066
+ } else if (net > 0n && EARNED_ACTIVITY_TYPES.has(transaction.activityType)) {
2067
+ bucket = "earned";
2068
+ amount = net;
2069
+ } else if (net < 0n && (transaction.activityType === "allocation" || transaction.activityType === "boost")) {
2070
+ bucket = "allocated";
2071
+ amount = -net;
2072
+ } else if (net > 0n && transaction.activityType === "reclaim") {
2073
+ bucket = "reclaimed";
2074
+ amount = net;
2075
+ } else if (net < 0n && transaction.activityType === "spend") {
2076
+ bucket = "spent";
2077
+ amount = -net;
2078
+ } else if (net < 0n && (transaction.activityType === "refund" || transaction.activityType === "chargeback" || transaction.activityType === "reversal")) {
2079
+ bucket = "reversed";
2080
+ amount = -net;
2081
+ }
2082
+ if (bucket !== void 0) {
2083
+ deltas.push({
2084
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2085
+ walletId,
2086
+ bucket,
2087
+ amount: serializeTokenSubunits(amount)
2088
+ });
2089
+ }
2090
+ }
2091
+ return deltas;
2092
+ }
2093
+ function applyWalletLifetimeDeltas(current, walletId, deltas) {
2094
+ assertWalletLifetimeTotals(current);
2095
+ assertEconomyIdentifier(walletId, "walletId");
2096
+ const next = {
2097
+ bought: parseTokenSubunits(current.bought),
2098
+ earned: parseTokenSubunits(current.earned),
2099
+ allocated: parseTokenSubunits(current.allocated),
2100
+ reclaimed: parseTokenSubunits(current.reclaimed),
2101
+ spent: parseTokenSubunits(current.spent),
2102
+ reversed: parseTokenSubunits(current.reversed)
2103
+ };
2104
+ for (const delta of deltas) {
2105
+ economyAssert(
2106
+ delta.schemaVersion === ECONOMY_CONTRACT_VERSION && delta.walletId === walletId && [
2107
+ "bought",
2108
+ "earned",
2109
+ "allocated",
2110
+ "reclaimed",
2111
+ "spent",
2112
+ "reversed"
2113
+ ].includes(delta.bucket),
2114
+ "INVALID_CONTRACT",
2115
+ "Lifetime delta must target the requested wallet and a known bucket"
2116
+ );
2117
+ const amount = parseTokenSubunits(delta.amount);
2118
+ economyAssert(
2119
+ amount > 0n,
2120
+ "INVALID_AMOUNT",
2121
+ "Lifetime deltas must be positive"
2122
+ );
2123
+ next[delta.bucket] += amount;
2124
+ }
2125
+ const totals = {
2126
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2127
+ bought: serializeTokenSubunits(next.bought),
2128
+ earned: serializeTokenSubunits(next.earned),
2129
+ allocated: serializeTokenSubunits(next.allocated),
2130
+ reclaimed: serializeTokenSubunits(next.reclaimed),
2131
+ spent: serializeTokenSubunits(next.spent),
2132
+ reversed: serializeTokenSubunits(next.reversed)
2133
+ };
2134
+ assertWalletLifetimeTotals(totals);
2135
+ return totals;
2136
+ }
2137
+ function applyTransactionToProjection(current, transaction) {
2138
+ assertBalancedTransaction(transaction);
2139
+ const next = { ...current };
2140
+ for (const posting of transaction.postings) {
2141
+ const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
2142
+ next[posting.accountId] = serializeTokenSubunits(
2143
+ prior + parseTokenSubunits(posting.amount)
2144
+ );
2145
+ }
2146
+ return next;
2147
+ }
2148
+ function rebuildBalanceProjection(transactions) {
2149
+ let projection = {};
2150
+ const transactionIds = /* @__PURE__ */ new Set();
2151
+ const idempotencyKeys = /* @__PURE__ */ new Set();
2152
+ const providerEvents = /* @__PURE__ */ new Set();
2153
+ for (const transaction of transactions) {
2154
+ economyAssert(
2155
+ !transactionIds.has(transaction.transactionId),
2156
+ "DUPLICATE_TRANSACTION",
2157
+ "Transaction identifiers must be unique"
2158
+ );
2159
+ economyAssert(
2160
+ !idempotencyKeys.has(transaction.idempotencyKey),
2161
+ "DUPLICATE_TRANSACTION",
2162
+ "Idempotency keys must be unique"
2163
+ );
2164
+ if (transaction.providerEventId !== void 0) {
2165
+ economyAssert(
2166
+ !providerEvents.has(transaction.providerEventId),
2167
+ "DUPLICATE_TRANSACTION",
2168
+ "Provider event identifiers must be unique"
2169
+ );
2170
+ providerEvents.add(transaction.providerEventId);
2171
+ }
2172
+ transactionIds.add(transaction.transactionId);
2173
+ idempotencyKeys.add(transaction.idempotencyKey);
2174
+ projection = applyTransactionToProjection(projection, transaction);
2175
+ }
2176
+ return projection;
2177
+ }
2178
+ function assertNonNegativeAccounts(projection, protectedAccountIds) {
2179
+ for (const accountId of protectedAccountIds) {
2180
+ economyAssert(
2181
+ parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
2182
+ "NEGATIVE_PROJECTION",
2183
+ "A protected economy account would become negative"
2184
+ );
2185
+ }
2186
+ }
2187
+ function createProjectionSnapshot(transactions, rebuiltAt) {
2188
+ parseIsoTimestamp(rebuiltAt);
2189
+ const last = transactions.at(-1);
2190
+ return {
2191
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2192
+ balances: rebuildBalanceProjection(transactions),
2193
+ ...last === void 0 ? {} : { lastTransactionId: last.transactionId },
2194
+ rebuiltAt
2195
+ };
2196
+ }
2197
+
2198
+ // src/ports/query.ts
2199
+ var ACTIVITY_TYPES2 = /* @__PURE__ */ new Set([
2200
+ "purchase",
2201
+ "subscription",
2202
+ "rewarded-ad",
2203
+ "offerwall",
2204
+ "allocation",
2205
+ "boost",
2206
+ "reclaim",
2207
+ "spend",
2208
+ "hold",
2209
+ "refund",
2210
+ "chargeback",
2211
+ "adjustment",
2212
+ "reversal",
2213
+ "event",
2214
+ "competition"
2215
+ ]);
2216
+ var ACTIVITY_STATUSES2 = /* @__PURE__ */ new Set([
2217
+ "pending",
2218
+ "held",
2219
+ "settled",
2220
+ "reversed",
2221
+ "failed"
2222
+ ]);
2223
+ var ACTIVITY_SOURCES2 = /* @__PURE__ */ new Set([
2224
+ "shopify",
2225
+ "ayet",
2226
+ "bitlabs",
2227
+ "subscription",
2228
+ "event",
2229
+ "competition",
2230
+ "adjustment"
2231
+ ]);
2232
+ var PORTFOLIO_ROLES = /* @__PURE__ */ new Set([
2233
+ "household-treasury",
2234
+ "personal",
2235
+ "gameplay-allocation",
2236
+ "hold"
2237
+ ]);
2238
+ function hasControlCharacters2(value) {
2239
+ return [...value].some((character) => {
2240
+ const code = character.charCodeAt(0);
2241
+ return code <= 31 || code === 127;
2242
+ });
2243
+ }
2244
+ function assertSafeDisplayString(value, label) {
2245
+ economyAssert(
2246
+ typeof value === "string" && value.trim().length > 0 && value.length <= 128 && !hasControlCharacters2(value),
2247
+ "INVALID_CONTRACT",
2248
+ `${label} must be bounded and safe for display`
2249
+ );
2250
+ }
2251
+ function assertOpaqueCursor(value) {
2252
+ economyAssert(
2253
+ typeof value === "string" && value.length > 0 && value.length <= 512 && !hasControlCharacters2(value),
2254
+ "INVALID_CONTRACT",
2255
+ "Activity cursor must be a bounded opaque value"
2256
+ );
2257
+ }
2258
+ function assertUniqueValues(values, allowed, label) {
2259
+ const unique = new Set(values);
2260
+ economyAssert(
2261
+ values.length > 0 && values.length === unique.size && values.every((value) => allowed.has(value)),
2262
+ "INVALID_CONTRACT",
2263
+ `${label} must contain unique supported values`
2264
+ );
2265
+ }
2266
+ function assertWalletPortfolioReadScope(scope) {
2267
+ economyAssert(
2268
+ scope.schemaVersion === ECONOMY_CONTRACT_VERSION,
2269
+ "INVALID_CONTRACT",
2270
+ "Unsupported wallet-portfolio scope contract version"
2271
+ );
2272
+ assertEconomyIdentifier(scope.portfolioId, "portfolioId");
2273
+ assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
2274
+ economyAssert(
2275
+ scope.components.length > 0 && scope.components.length <= 100,
2276
+ "INVALID_CONTRACT",
2277
+ "Wallet portfolio must contain between one and 100 components"
2278
+ );
2279
+ const walletIds = /* @__PURE__ */ new Set();
2280
+ for (const component of scope.components) {
2281
+ assertEconomyIdentifier(component.walletId, "walletId");
2282
+ economyAssert(
2283
+ PORTFOLIO_ROLES.has(component.role),
2284
+ "INVALID_CONTRACT",
2285
+ "Wallet portfolio component has an unsupported role"
2286
+ );
2287
+ economyAssert(
2288
+ !walletIds.has(component.walletId),
2289
+ "DUPLICATE_IDENTIFIER",
2290
+ "Wallet portfolio cannot repeat a wallet"
2291
+ );
2292
+ walletIds.add(component.walletId);
2293
+ if (component.beneficiaryAccountId !== void 0) {
2294
+ assertEconomyIdentifier(
2295
+ component.beneficiaryAccountId,
2296
+ "beneficiaryAccountId"
2297
+ );
2298
+ }
2299
+ economyAssert(
2300
+ component.role !== "gameplay-allocation" || component.beneficiaryAccountId !== void 0,
2301
+ "INVALID_CONTRACT",
2302
+ "Gameplay-allocation portfolio components require a beneficiary"
2303
+ );
2304
+ }
2305
+ }
2306
+ function assertWalletLifetimeSnapshot(snapshot) {
2307
+ economyAssert(
2308
+ snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
2309
+ "INVALID_CONTRACT",
2310
+ "Unsupported wallet-lifetime-snapshot contract version"
2311
+ );
2312
+ assertEconomyIdentifier(snapshot.walletId, "walletId");
2313
+ assertWalletLifetimeTotals(snapshot.totals);
2314
+ economyAssert(
2315
+ Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
2316
+ "INVALID_CONTRACT",
2317
+ "Wallet lifetime version must be a positive safe integer"
2318
+ );
2319
+ parseIsoTimestamp(snapshot.asOf);
2320
+ }
2321
+ function assertPortfolioComponentMatch(expected, actual) {
2322
+ economyAssert(
2323
+ expected.walletId === actual.walletId && expected.role === actual.role && expected.beneficiaryAccountId === actual.beneficiaryAccountId,
2324
+ "INVALID_CONTRACT",
2325
+ "Portfolio result component does not match its authorized read scope"
2326
+ );
2327
+ }
2328
+ function createWalletPortfolioSummary(scope, components, asOf) {
2329
+ assertWalletPortfolioReadScope(scope);
2330
+ parseIsoTimestamp(asOf);
2331
+ economyAssert(
2332
+ components.length === scope.components.length,
2333
+ "INVALID_CONTRACT",
2334
+ "Portfolio balance must include every authorized component exactly once"
2335
+ );
2336
+ const byWallet = new Map(components.map((item) => [item.walletId, item]));
2337
+ economyAssert(
2338
+ byWallet.size === components.length,
2339
+ "DUPLICATE_IDENTIFIER",
2340
+ "Portfolio balance cannot repeat a wallet"
2341
+ );
2342
+ const ordered = scope.components.map((expected) => {
2343
+ const actual = byWallet.get(expected.walletId);
2344
+ economyAssert(
2345
+ actual !== void 0,
2346
+ "INVALID_CONTRACT",
2347
+ "Portfolio balance is missing an authorized wallet"
2348
+ );
2349
+ assertPortfolioComponentMatch(expected, actual);
2350
+ assertDeterministicWalletBalanceSummary(actual.summary);
2351
+ economyAssert(
2352
+ actual.summary.walletId === actual.walletId && actual.summary.asOf === asOf,
2353
+ "INVALID_CONTRACT",
2354
+ "Portfolio component summary must identify its wallet and shared snapshot time"
2355
+ );
2356
+ return actual;
2357
+ });
2358
+ const totals = {
2359
+ available: serializeTokenSubunits(
2360
+ ordered.reduce(
2361
+ (sum, item) => sum + parseTokenSubunits(item.summary.available),
2362
+ 0n
2363
+ )
2364
+ ),
2365
+ reserved: serializeTokenSubunits(
2366
+ ordered.reduce(
2367
+ (sum, item) => sum + parseTokenSubunits(item.summary.reserved),
2368
+ 0n
2369
+ )
2370
+ ),
2371
+ held: serializeTokenSubunits(
2372
+ ordered.reduce(
2373
+ (sum, item) => sum + parseTokenSubunits(item.summary.held),
2374
+ 0n
2375
+ )
2376
+ ),
2377
+ rewardProgress: serializeTokenSubunits(
2378
+ ordered.reduce(
2379
+ (sum, item) => sum + parseTokenSubunits(item.summary.rewardProgress),
2380
+ 0n
2381
+ )
2382
+ )
2383
+ };
2384
+ return {
2385
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2386
+ portfolioId: scope.portfolioId,
2387
+ subjectAccountId: scope.subjectAccountId,
2388
+ components: ordered,
2389
+ totals,
2390
+ asOf
2391
+ };
2392
+ }
2393
+ function assertWalletPortfolioSummary(summary) {
2394
+ const scope = {
2395
+ schemaVersion: summary.schemaVersion,
2396
+ portfolioId: summary.portfolioId,
2397
+ subjectAccountId: summary.subjectAccountId,
2398
+ components: summary.components.map(
2399
+ ({ walletId, role, beneficiaryAccountId }) => ({
2400
+ walletId,
2401
+ role,
2402
+ ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
2403
+ })
2404
+ )
2405
+ };
2406
+ const rebuilt = createWalletPortfolioSummary(
2407
+ scope,
2408
+ summary.components,
2409
+ summary.asOf
2410
+ );
2411
+ for (const field of [
2412
+ "available",
2413
+ "reserved",
2414
+ "held",
2415
+ "rewardProgress"
2416
+ ]) {
2417
+ economyAssert(
2418
+ summary.totals[field] === rebuilt.totals[field],
2419
+ "INVALID_CONTRACT",
2420
+ "Portfolio balance totals must equal their component column sums"
2421
+ );
2422
+ }
2423
+ }
2424
+ function createWalletPortfolioLifetime(scope, components, asOf) {
2425
+ assertWalletPortfolioReadScope(scope);
2426
+ parseIsoTimestamp(asOf);
2427
+ economyAssert(
2428
+ components.length === scope.components.length,
2429
+ "INVALID_CONTRACT",
2430
+ "Portfolio lifetime must include every authorized component exactly once"
2431
+ );
2432
+ const byWallet = new Map(components.map((item) => [item.walletId, item]));
2433
+ economyAssert(
2434
+ byWallet.size === components.length,
2435
+ "DUPLICATE_IDENTIFIER",
2436
+ "Portfolio lifetime cannot repeat a wallet"
2437
+ );
2438
+ const ordered = scope.components.map((expected) => {
2439
+ const actual = byWallet.get(expected.walletId);
2440
+ economyAssert(
2441
+ actual !== void 0,
2442
+ "INVALID_CONTRACT",
2443
+ "Portfolio lifetime is missing an authorized wallet"
2444
+ );
2445
+ assertPortfolioComponentMatch(expected, actual);
2446
+ assertWalletLifetimeSnapshot(actual.snapshot);
2447
+ economyAssert(
2448
+ actual.snapshot.walletId === actual.walletId && actual.snapshot.asOf === asOf,
2449
+ "INVALID_CONTRACT",
2450
+ "Portfolio lifetime component must identify its wallet and shared snapshot time"
2451
+ );
2452
+ return actual;
2453
+ });
2454
+ const sumField = (field) => serializeTokenSubunits(
2455
+ ordered.reduce(
2456
+ (sum, item) => sum + parseTokenSubunits(item.snapshot.totals[field]),
2457
+ 0n
2458
+ )
2459
+ );
2460
+ const totals = {
2461
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2462
+ bought: sumField("bought"),
2463
+ earned: sumField("earned"),
2464
+ allocated: sumField("allocated"),
2465
+ reclaimed: sumField("reclaimed"),
2466
+ spent: sumField("spent"),
2467
+ reversed: sumField("reversed")
2468
+ };
2469
+ assertWalletLifetimeTotals(totals);
2470
+ return {
2471
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2472
+ portfolioId: scope.portfolioId,
2473
+ subjectAccountId: scope.subjectAccountId,
2474
+ components: ordered,
2475
+ totals,
2476
+ asOf
2477
+ };
2478
+ }
2479
+ function assertWalletPortfolioLifetime(lifetime) {
2480
+ const scope = {
2481
+ schemaVersion: lifetime.schemaVersion,
2482
+ portfolioId: lifetime.portfolioId,
2483
+ subjectAccountId: lifetime.subjectAccountId,
2484
+ components: lifetime.components.map(
2485
+ ({ walletId, role, beneficiaryAccountId }) => ({
2486
+ walletId,
2487
+ role,
2488
+ ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
2489
+ })
2490
+ )
2491
+ };
2492
+ const rebuilt = createWalletPortfolioLifetime(
2493
+ scope,
2494
+ lifetime.components,
2495
+ lifetime.asOf
2496
+ );
2497
+ for (const field of [
2498
+ "bought",
2499
+ "earned",
2500
+ "allocated",
2501
+ "reclaimed",
2502
+ "spent",
2503
+ "reversed"
2504
+ ]) {
2505
+ economyAssert(
2506
+ lifetime.totals[field] === rebuilt.totals[field],
2507
+ "INVALID_CONTRACT",
2508
+ "Portfolio lifetime totals must equal their component column sums"
2509
+ );
2510
+ }
2511
+ }
2512
+ function assertWalletActivityEntry(entry) {
2513
+ economyAssert(
2514
+ entry.schemaVersion === ECONOMY_CONTRACT_VERSION,
2515
+ "INVALID_CONTRACT",
2516
+ "Unsupported wallet-activity contract version"
2517
+ );
2518
+ assertEconomyIdentifier(entry.activityId, "activityId");
2519
+ assertEconomyIdentifier(entry.walletId, "walletId");
2520
+ economyAssert(
2521
+ ACTIVITY_TYPES2.has(entry.activityType) && ACTIVITY_STATUSES2.has(entry.status) && ACTIVITY_SOURCES2.has(entry.source),
2522
+ "INVALID_CONTRACT",
2523
+ "Wallet activity type, status, or source is unsupported"
2524
+ );
2525
+ parseIsoTimestamp(entry.occurredAt);
2526
+ economyAssert(
2527
+ parseTokenSubunits(entry.amount) !== 0n,
2528
+ "INVALID_AMOUNT",
2529
+ "Wallet activity amount cannot be zero"
2530
+ );
2531
+ if (entry.beneficiaryAccountId !== void 0) {
2532
+ assertEconomyIdentifier(
2533
+ entry.beneficiaryAccountId,
2534
+ "beneficiaryAccountId"
2535
+ );
2536
+ }
2537
+ if (entry.maskedReference !== void 0) {
2538
+ assertSafeDisplayString(entry.maskedReference, "Masked reference");
2539
+ }
2540
+ assertSafeDisplayString(entry.sourceLabel, "Source label");
2541
+ economyAssert(
2542
+ entry.entryKind === "economic" || entry.entryKind === "workflow",
2543
+ "INVALID_CONTRACT",
2544
+ "Wallet activity has an unsupported entry kind"
2545
+ );
2546
+ if (entry.entryKind === "economic") {
2547
+ assertEconomyIdentifier(entry.transactionId, "transactionId");
2548
+ economyAssert(
2549
+ entry.status === "held" || entry.status === "settled" || entry.status === "reversed",
2550
+ "INVALID_CONTRACT",
2551
+ "Economic activity cannot use a workflow status"
2552
+ );
2553
+ } else {
2554
+ assertEconomyIdentifier(entry.commandId, "commandId");
2555
+ economyAssert(
2556
+ entry.status === "pending" || entry.status === "failed",
2557
+ "INVALID_CONTRACT",
2558
+ "Workflow activity cannot use an economic status"
2559
+ );
2560
+ }
2561
+ }
2562
+ function assertWalletActivityPageRequest(request) {
2563
+ economyAssert(
2564
+ Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 100,
2565
+ "INVALID_CONTRACT",
2566
+ "Activity page limit must be between one and 100"
2567
+ );
2568
+ if (request.cursor !== void 0) {
2569
+ assertOpaqueCursor(request.cursor);
2570
+ }
2571
+ const filter = request.filter;
2572
+ if (filter === void 0) {
2573
+ return;
2574
+ }
2575
+ if (filter.activityTypes !== void 0) {
2576
+ assertUniqueValues(filter.activityTypes, ACTIVITY_TYPES2, "Activity types");
2577
+ }
2578
+ if (filter.statuses !== void 0) {
2579
+ assertUniqueValues(filter.statuses, ACTIVITY_STATUSES2, "Activity statuses");
2580
+ }
2581
+ if (filter.sources !== void 0) {
2582
+ assertUniqueValues(filter.sources, ACTIVITY_SOURCES2, "Activity sources");
2583
+ }
2584
+ if (filter.beneficiaryAccountId !== void 0) {
2585
+ assertEconomyIdentifier(
2586
+ filter.beneficiaryAccountId,
2587
+ "beneficiaryAccountId"
2588
+ );
2589
+ }
2590
+ const from = filter.fromInclusive === void 0 ? void 0 : parseIsoTimestamp(filter.fromInclusive);
2591
+ const to = filter.toExclusive === void 0 ? void 0 : parseIsoTimestamp(filter.toExclusive);
2592
+ economyAssert(
2593
+ from === void 0 || to === void 0 || from < to,
2594
+ "INVALID_TIME_WINDOW",
2595
+ "Activity filter start must precede its exclusive end"
2596
+ );
2597
+ }
2598
+ function assertWalletActivityPage(page) {
2599
+ economyAssert(
2600
+ page.schemaVersion === ECONOMY_CONTRACT_VERSION && typeof page.hasMore === "boolean",
2601
+ "INVALID_CONTRACT",
2602
+ "Unsupported wallet-activity-page contract"
2603
+ );
2604
+ economyAssert(
2605
+ page.hasMore === (page.nextCursor !== void 0),
2606
+ "INVALID_CONTRACT",
2607
+ "Activity page cursor must be present exactly when more rows exist"
2608
+ );
2609
+ economyAssert(
2610
+ page.entries.length <= 100 && (!page.hasMore || page.entries.length > 0),
2611
+ "INVALID_CONTRACT",
2612
+ "Activity pages are bounded and cannot advertise more rows from an empty page"
2613
+ );
2614
+ if (page.nextCursor !== void 0) {
2615
+ assertOpaqueCursor(page.nextCursor);
2616
+ }
2617
+ const activityIds = /* @__PURE__ */ new Set();
2618
+ let previous;
2619
+ for (const entry of page.entries) {
2620
+ assertWalletActivityEntry(entry);
2621
+ economyAssert(
2622
+ !activityIds.has(entry.activityId),
2623
+ "DUPLICATE_IDENTIFIER",
2624
+ "Activity page cannot repeat an activity ID"
2625
+ );
2626
+ activityIds.add(entry.activityId);
2627
+ if (previous !== void 0) {
2628
+ const previousTime = parseIsoTimestamp(previous.occurredAt);
2629
+ const currentTime = parseIsoTimestamp(entry.occurredAt);
2630
+ economyAssert(
2631
+ previousTime > currentTime || previousTime === currentTime && previous.activityId.localeCompare(entry.activityId) > 0,
2632
+ "INVALID_CONTRACT",
2633
+ "Activity page must use stable descending time and ID order"
2634
+ );
2635
+ }
2636
+ previous = entry;
2637
+ }
2638
+ }
2639
+ function assertWalletActivityPageForPortfolio(page, scope) {
2640
+ assertWalletActivityPage(page);
2641
+ assertWalletPortfolioReadScope(scope);
2642
+ const walletIds = new Set(scope.components.map((item) => item.walletId));
2643
+ economyAssert(
2644
+ page.entries.every((entry) => walletIds.has(entry.walletId)),
2645
+ "INVALID_CONTRACT",
2646
+ "Portfolio activity contains a wallet outside its authorized read scope"
2647
+ );
2648
+ }
2649
+
2650
+ // src/recurrence.ts
2651
+ var BASELINE_MONTHLY_SUBSCRIPTION_PLAN = Object.freeze({
2652
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2653
+ planId: "monthly-10-100-v1",
2654
+ catalogVersion: "gbp-subscriptions-v1",
2655
+ interval: "monthly",
2656
+ priceMinorUnits: "1000",
2657
+ currency: "GBP",
2658
+ grantAmount: serializeTokenSubunits(100000n),
2659
+ enabled: false,
2660
+ effectiveFrom: "2026-07-15T00:00:00.000Z"
2661
+ });
2662
+ function assertTokenSubscriptionPlan(plan) {
2663
+ economyAssert(
2664
+ plan.schemaVersion === ECONOMY_CONTRACT_VERSION,
2665
+ "INVALID_CONTRACT",
2666
+ "Unsupported subscription-plan contract version"
2667
+ );
2668
+ assertEconomyIdentifier(plan.planId, "planId");
2669
+ assertEconomyIdentifier(plan.catalogVersion, "catalogVersion");
2670
+ economyAssert(
2671
+ ["daily", "weekly", "monthly"].includes(plan.interval) && plan.currency === "GBP" && typeof plan.enabled === "boolean",
2672
+ "INVALID_CONTRACT",
2673
+ "Subscription recurrence, currency, or enabled state is invalid"
2674
+ );
2675
+ economyAssert(
2676
+ /^(?:0|[1-9][0-9]*)$/u.test(plan.priceMinorUnits) && BigInt(plan.priceMinorUnits) > 0n,
2677
+ "INVALID_AMOUNT",
2678
+ "Subscription price must be a positive integer minor-unit string"
2679
+ );
2680
+ const grantAmount = parseTokenSubunits(plan.grantAmount);
2681
+ economyAssert(
2682
+ grantAmount > 0n && isWholeTokenAmount(grantAmount),
2683
+ "AMOUNT_NOT_WHOLE_TOKEN",
2684
+ "Subscription grants must contain whole Tokens"
2685
+ );
2686
+ parseIsoTimestamp(plan.effectiveFrom);
2687
+ }
2688
+
2689
+ // src/spend-requests.ts
2690
+ function assertSpendRequest(request) {
2691
+ economyAssert(
2692
+ request.schemaVersion === ECONOMY_CONTRACT_VERSION,
2693
+ "INVALID_CONTRACT",
2694
+ "Unsupported spend-request contract version"
2695
+ );
2696
+ assertEconomyIdentifier(request.requestId, "requestId");
2697
+ assertEconomyIdentifier(request.childAccountId, "childAccountId");
2698
+ assertEconomyIdentifier(request.allocationId, "allocationId");
2699
+ economyAssert(
2700
+ request.purpose === "gameplay-conversion" || request.purpose === "non-gameplay",
2701
+ "INVALID_CONTRACT",
2702
+ "Spend request has an unsupported purpose"
2703
+ );
2704
+ economyAssert(
2705
+ [
2706
+ "requested",
2707
+ "approved",
2708
+ "rejected",
2709
+ "expired",
2710
+ "executed",
2711
+ "cancelled"
2712
+ ].includes(request.status),
2713
+ "INVALID_CONTRACT",
2714
+ "Spend request has an unsupported status"
2715
+ );
2716
+ economyAssert(
2717
+ parseTokenSubunits(request.amount) > 0n,
2718
+ "INVALID_AMOUNT",
2719
+ "Spend-request amount must be positive"
2720
+ );
2721
+ const requestedAt = parseIsoTimestamp(request.requestedAt);
2722
+ const expiresAt = parseIsoTimestamp(request.expiresAt);
2723
+ economyAssert(
2724
+ expiresAt > requestedAt,
2725
+ "INVALID_TIME_WINDOW",
2726
+ "Spend-request expiry must follow creation"
2727
+ );
2728
+ const decisionRequired = ["approved", "rejected", "executed"].includes(
2729
+ request.status
2730
+ );
2731
+ economyAssert(
2732
+ !decisionRequired || request.decidedByAccountId !== void 0 && request.decidedAt !== void 0,
2733
+ "INVALID_CONTRACT",
2734
+ "Decided spend requests require a guardian actor and timestamp"
2735
+ );
2736
+ if (request.decidedByAccountId !== void 0) {
2737
+ assertEconomyIdentifier(request.decidedByAccountId, "decidedByAccountId");
2738
+ }
2739
+ if (request.decidedAt !== void 0) {
2740
+ const decidedAt = parseIsoTimestamp(request.decidedAt);
2741
+ economyAssert(
2742
+ decidedAt >= requestedAt && decidedAt <= expiresAt,
2743
+ "INVALID_TIME_WINDOW",
2744
+ "Spend-request decision must occur before expiry"
2745
+ );
2746
+ }
2747
+ if (request.approvalNonceHash !== void 0) {
2748
+ economyAssert(
2749
+ /^sha256:[a-f0-9]{64}$/u.test(request.approvalNonceHash),
2750
+ "INVALID_CONTRACT",
2751
+ "Approval nonces must be stored as SHA-256 hashes"
2752
+ );
2753
+ }
2754
+ if (request.status === "approved" || request.status === "executed") {
2755
+ economyAssert(
2756
+ request.approvalNonceHash !== void 0,
2757
+ "INVALID_CONTRACT",
2758
+ "Approved spend requests require a one-time approval nonce hash"
2759
+ );
2760
+ }
2761
+ if (request.resultingTransactionId !== void 0) {
2762
+ assertEconomyIdentifier(
2763
+ request.resultingTransactionId,
2764
+ "resultingTransactionId"
2765
+ );
2766
+ economyAssert(
2767
+ request.status === "executed",
2768
+ "INVALID_CONTRACT",
2769
+ "Only executed spend requests may reference a ledger transaction"
2770
+ );
2771
+ }
2772
+ }
1569
2773
  export {
1570
2774
  BASELINE_GBP_CATALOG_VERSION,
1571
2775
  BASELINE_GBP_REFERENCE_RATE,
1572
2776
  BASELINE_GBP_TOKEN_PACKS,
1573
2777
  BASELINE_MONTHLY_SUBSCRIPTION_PLAN,
1574
2778
  BASELINE_PURCHASE_LIMIT_POLICY,
2779
+ EARLY_BACKER_SETTLEMENT_POLICY_V2,
1575
2780
  ECONOMY_CONTRACT_VERSION,
1576
2781
  EconomyError,
1577
2782
  PRE_UTILITY_BACKER_COHORT,
1578
2783
  TOKEN_SUBUNITS_MAX,
1579
2784
  TOKEN_SUBUNITS_MIN,
1580
2785
  TOKEN_SUBUNITS_PER_TOKEN,
2786
+ advanceJournalChainHead,
2787
+ applySourceLotMovement,
1581
2788
  applyTransactionToProjection,
2789
+ applyWalletBalanceDelta,
2790
+ applyWalletLifetimeDeltas,
1582
2791
  assertActivityEntry,
1583
2792
  assertBalancedTransaction,
2793
+ assertDeterministicWalletBalanceSummary,
1584
2794
  assertDistinctAdjustmentApproval,
2795
+ assertEconomicJournalTransaction,
2796
+ assertEconomyCommandEnvelope,
2797
+ assertEconomyCommandWorkflowEvent,
2798
+ assertEconomyIdempotencyScope,
1585
2799
  assertEconomyIdentifier,
1586
2800
  assertFlatTokenCatalog,
1587
2801
  assertFxSnapshot,
1588
2802
  assertGameplayAllocation,
2803
+ assertJournalChainHead,
1589
2804
  assertNonNegativeAccounts,
1590
2805
  assertOpenPurchaseIntent,
1591
2806
  assertPurchaseIntentBinding,
@@ -1593,24 +2808,46 @@ export {
1593
2808
  assertReversalAvailable,
1594
2809
  assertRewardConversion,
1595
2810
  assertRewardRate,
2811
+ assertScopedPersistedIdempotencyResult,
1596
2812
  assertSourceLot,
2813
+ assertSourceLotMovement,
1597
2814
  assertSpendRequest,
1598
2815
  assertTokenPack,
1599
2816
  assertTokenReferenceRate,
1600
2817
  assertTokenSubscriptionPlan,
2818
+ assertVersionedSourceLot,
1601
2819
  assertWallet,
2820
+ assertWalletActivityEntry,
2821
+ assertWalletActivityPage,
2822
+ assertWalletActivityPageForPortfolio,
2823
+ assertWalletActivityPageRequest,
2824
+ assertWalletBalanceDelta,
2825
+ assertWalletBalanceDeltaBatch,
2826
+ assertWalletBalanceProjection,
1602
2827
  assertWalletBalanceSummary,
2828
+ assertWalletLifetimeSnapshot,
1603
2829
  assertWalletLifetimeTotals,
2830
+ assertWalletOwnerReference,
2831
+ assertWalletPortfolioLifetime,
2832
+ assertWalletPortfolioReadScope,
2833
+ assertWalletPortfolioSummary,
1604
2834
  availableSourceLotAmount,
1605
2835
  boostGameplayAllocation,
1606
2836
  canUseSourceLot,
1607
2837
  canonicalTransactionPayload,
1608
2838
  convertRewardPayout,
1609
2839
  createGameplayAllocation,
2840
+ createInitialSourceLotSnapshot,
1610
2841
  createProjectionSnapshot,
1611
2842
  createReversalTransaction,
2843
+ createWalletBalanceSummary,
2844
+ createWalletPortfolioLifetime,
2845
+ createWalletPortfolioSummary,
2846
+ deriveWalletBalanceDeltas,
2847
+ deriveWalletLifetimeDeltas,
1612
2848
  economyAssert,
1613
2849
  evaluateEarlyBacker,
2850
+ evaluateEarlyBackerBySettlementV2,
1614
2851
  isWholeTokenAmount,
1615
2852
  mapBitLabsConversionState,
1616
2853
  parseIsoTimestamp,