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