@plasius/economy 0.3.1 → 0.4.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/README.md +90 -5
- package/dist/index.cjs +2896 -839
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +443 -1
- package/dist/index.d.ts +443 -1
- package/dist/index.js +2859 -839
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1546,1117 +1546,3100 @@ function selectSourceLots(lots, requestedAmount, context) {
|
|
|
1546
1546
|
);
|
|
1547
1547
|
}
|
|
1548
1548
|
|
|
1549
|
-
// src/
|
|
1550
|
-
|
|
1549
|
+
// src/module-allowances.ts
|
|
1550
|
+
var MODULE_ALLOWANCE_PURPOSE = "junior-coder-module-entitlement";
|
|
1551
|
+
function assertWholePositive2(amount, label) {
|
|
1552
|
+
const parsed = parseTokenSubunits(amount);
|
|
1553
|
+
economyAssert(parsed > 0n, "INVALID_AMOUNT", `${label} must be positive`);
|
|
1551
1554
|
economyAssert(
|
|
1552
|
-
|
|
1555
|
+
isWholeTokenAmount(parsed),
|
|
1556
|
+
"AMOUNT_NOT_WHOLE_TOKEN",
|
|
1557
|
+
`${label} must contain whole Tokens`
|
|
1558
|
+
);
|
|
1559
|
+
return parsed;
|
|
1560
|
+
}
|
|
1561
|
+
function assertPositiveSlices(slices, expectedAmount, label) {
|
|
1562
|
+
economyAssert(slices.length > 0, "INVALID_CONTRACT", `${label} are required`);
|
|
1563
|
+
const lotIds = /* @__PURE__ */ new Set();
|
|
1564
|
+
for (const slice of slices) {
|
|
1565
|
+
assertEconomyIdentifier(slice.lotId, "lotId");
|
|
1566
|
+
economyAssert(
|
|
1567
|
+
!lotIds.has(slice.lotId),
|
|
1568
|
+
"DUPLICATE_IDENTIFIER",
|
|
1569
|
+
`${label} must not repeat a lot`
|
|
1570
|
+
);
|
|
1571
|
+
lotIds.add(slice.lotId);
|
|
1572
|
+
economyAssert(
|
|
1573
|
+
parseTokenSubunits(slice.amount) > 0n,
|
|
1574
|
+
"INVALID_AMOUNT",
|
|
1575
|
+
`${label} amounts must be positive`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
economyAssert(
|
|
1579
|
+
sumTokenSubunits(slices.map((slice) => slice.amount)) === expectedAmount,
|
|
1553
1580
|
"INVALID_CONTRACT",
|
|
1554
|
-
|
|
1581
|
+
`${label} must sum to the operation amount`
|
|
1555
1582
|
);
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1583
|
+
}
|
|
1584
|
+
function sumSlicesByLot(slices) {
|
|
1585
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1586
|
+
for (const slice of slices) {
|
|
1587
|
+
assertEconomyIdentifier(slice.lotId, "lotId");
|
|
1588
|
+
const amount = parseTokenSubunits(slice.amount);
|
|
1589
|
+
economyAssert(amount > 0n, "INVALID_AMOUNT", "Lot slices must be positive");
|
|
1590
|
+
totals.set(slice.lotId, (totals.get(slice.lotId) ?? 0n) + amount);
|
|
1591
|
+
}
|
|
1592
|
+
return totals;
|
|
1593
|
+
}
|
|
1594
|
+
function assertSha256Reference(value, label) {
|
|
1559
1595
|
economyAssert(
|
|
1560
|
-
|
|
1561
|
-
"
|
|
1562
|
-
|
|
1596
|
+
typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value),
|
|
1597
|
+
"INVALID_CONTRACT",
|
|
1598
|
+
`${label} must be a canonical SHA-256 reference`
|
|
1563
1599
|
);
|
|
1600
|
+
}
|
|
1601
|
+
function assertUpdatedAt(current, occurredAt, label) {
|
|
1564
1602
|
economyAssert(
|
|
1565
|
-
|
|
1603
|
+
parseIsoTimestamp(occurredAt) >= parseIsoTimestamp(current),
|
|
1604
|
+
"INVALID_TIME_WINDOW",
|
|
1605
|
+
`${label} cannot precede the current state`
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
function assertModuleAllowance(allowance) {
|
|
1609
|
+
economyAssert(
|
|
1610
|
+
allowance.schemaVersion === ECONOMY_CONTRACT_VERSION && allowance.purpose === MODULE_ALLOWANCE_PURPOSE,
|
|
1566
1611
|
"INVALID_CONTRACT",
|
|
1567
|
-
"
|
|
1612
|
+
"Unsupported Module Allowance contract"
|
|
1568
1613
|
);
|
|
1569
|
-
assertEconomyIdentifier(
|
|
1570
|
-
|
|
1614
|
+
assertEconomyIdentifier(allowance.allowanceId, "allowanceId");
|
|
1615
|
+
assertEconomyIdentifier(allowance.householdId, "householdId");
|
|
1616
|
+
assertEconomyIdentifier(allowance.hostWalletId, "hostWalletId");
|
|
1617
|
+
assertEconomyIdentifier(allowance.allowanceWalletId, "allowanceWalletId");
|
|
1618
|
+
assertEconomyIdentifier(allowance.childAccountId, "childAccountId");
|
|
1571
1619
|
economyAssert(
|
|
1572
|
-
|
|
1620
|
+
allowance.hostWalletId !== allowance.allowanceWalletId,
|
|
1573
1621
|
"INVALID_CONTRACT",
|
|
1574
|
-
"
|
|
1622
|
+
"Host and Module Allowance wallets must be distinct"
|
|
1575
1623
|
);
|
|
1576
|
-
assertEconomyIdentifier(request.approvedByAccountId, "approvedByAccountId");
|
|
1577
1624
|
economyAssert(
|
|
1578
|
-
|
|
1625
|
+
allowance.status === "active" || allowance.status === "closed",
|
|
1626
|
+
"INVALID_CONTRACT",
|
|
1627
|
+
"Module Allowance status is unsupported"
|
|
1628
|
+
);
|
|
1629
|
+
const createdAt = parseIsoTimestamp(allowance.createdAt);
|
|
1630
|
+
const updatedAt = parseIsoTimestamp(allowance.updatedAt);
|
|
1631
|
+
economyAssert(
|
|
1632
|
+
updatedAt >= createdAt,
|
|
1579
1633
|
"INVALID_TIME_WINDOW",
|
|
1580
|
-
"
|
|
1634
|
+
"Module Allowance update cannot precede creation"
|
|
1581
1635
|
);
|
|
1582
1636
|
economyAssert(
|
|
1583
|
-
|
|
1637
|
+
Number.isSafeInteger(allowance.version) && allowance.version >= 1,
|
|
1584
1638
|
"INVALID_CONTRACT",
|
|
1585
|
-
"
|
|
1639
|
+
"Module Allowance version must be a positive safe integer"
|
|
1586
1640
|
);
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
// src/ports/persistence.ts
|
|
1596
|
-
function assertScopedPersistedIdempotencyResult(result) {
|
|
1641
|
+
const allocated = parseTokenSubunits(allowance.allocatedAmount);
|
|
1642
|
+
const available = parseTokenSubunits(allowance.availableAmount);
|
|
1643
|
+
const held = parseTokenSubunits(allowance.heldAmount);
|
|
1644
|
+
const spent = parseTokenSubunits(allowance.spentAmount);
|
|
1645
|
+
const reclaimed = parseTokenSubunits(allowance.reclaimedAmount);
|
|
1597
1646
|
economyAssert(
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1647
|
+
allocated > 0n && [allocated, available, held, spent, reclaimed].every(
|
|
1648
|
+
(amount) => amount >= 0n && isWholeTokenAmount(amount)
|
|
1649
|
+
),
|
|
1650
|
+
"AMOUNT_NOT_WHOLE_TOKEN",
|
|
1651
|
+
"Module Allowance amounts must be non-negative whole Tokens"
|
|
1601
1652
|
);
|
|
1602
|
-
assertEconomyIdentifier(result.commandId, "commandId");
|
|
1603
|
-
assertEconomyIdentifier(result.transactionId, "transactionId");
|
|
1604
1653
|
economyAssert(
|
|
1605
|
-
|
|
1654
|
+
allocated === available + held + spent + reclaimed,
|
|
1606
1655
|
"INVALID_CONTRACT",
|
|
1607
|
-
"
|
|
1656
|
+
"Module Allowance buckets must equal its lifetime allocation"
|
|
1608
1657
|
);
|
|
1609
|
-
parseIsoTimestamp(result.recordedAt);
|
|
1610
|
-
}
|
|
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
1658
|
economyAssert(
|
|
1630
|
-
|
|
1659
|
+
allowance.status !== "closed" || available === 0n && held === 0n,
|
|
1631
1660
|
"INVALID_CONTRACT",
|
|
1632
|
-
"
|
|
1661
|
+
"A closed Module Allowance cannot retain available or held value"
|
|
1633
1662
|
);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
|
|
1637
|
-
}
|
|
1638
|
-
function assertEconomyCommandEnvelope(envelope) {
|
|
1663
|
+
const fundedByLot = sumSlicesByLot(allowance.fundingSlices);
|
|
1664
|
+
const reclaimedByLot = sumSlicesByLot(allowance.reclaimedSlices);
|
|
1639
1665
|
economyAssert(
|
|
1640
|
-
|
|
1666
|
+
sumTokenSubunits(allowance.fundingSlices.map((slice) => slice.amount)) === allocated,
|
|
1641
1667
|
"INVALID_CONTRACT",
|
|
1642
|
-
"
|
|
1668
|
+
"Module Allowance funding provenance must equal allocated value"
|
|
1643
1669
|
);
|
|
1644
|
-
assertEconomyIdentifier(envelope.commandId, "commandId");
|
|
1645
1670
|
economyAssert(
|
|
1646
|
-
|
|
1671
|
+
sumTokenSubunits(allowance.reclaimedSlices.map((slice) => slice.amount)) === reclaimed,
|
|
1647
1672
|
"INVALID_CONTRACT",
|
|
1648
|
-
"
|
|
1673
|
+
"Module Allowance reclaim provenance must equal reclaimed value"
|
|
1649
1674
|
);
|
|
1650
|
-
|
|
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) {
|
|
1675
|
+
for (const [lotId, amount] of reclaimedByLot) {
|
|
1657
1676
|
economyAssert(
|
|
1658
|
-
|
|
1659
|
-
"
|
|
1660
|
-
"
|
|
1677
|
+
amount <= (fundedByLot.get(lotId) ?? 0n),
|
|
1678
|
+
"SOURCE_LOT_RESTRICTED",
|
|
1679
|
+
"Reclaimed provenance cannot exceed Module Allowance funding"
|
|
1661
1680
|
);
|
|
1662
1681
|
}
|
|
1682
|
+
}
|
|
1683
|
+
function createModuleAllowance(input) {
|
|
1684
|
+
const amount = assertWholePositive2(input.amount, "Module Allowance amount");
|
|
1685
|
+
assertPositiveSlices(input.fundingSlices, amount, "Funding slices");
|
|
1686
|
+
const allowance = {
|
|
1687
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
1688
|
+
purpose: MODULE_ALLOWANCE_PURPOSE,
|
|
1689
|
+
allowanceId: input.allowanceId,
|
|
1690
|
+
householdId: input.householdId,
|
|
1691
|
+
hostWalletId: input.hostWalletId,
|
|
1692
|
+
allowanceWalletId: input.allowanceWalletId,
|
|
1693
|
+
childAccountId: input.childAccountId,
|
|
1694
|
+
status: "active",
|
|
1695
|
+
allocatedAmount: input.amount,
|
|
1696
|
+
availableAmount: input.amount,
|
|
1697
|
+
heldAmount: serializeTokenSubunits(0n),
|
|
1698
|
+
spentAmount: serializeTokenSubunits(0n),
|
|
1699
|
+
reclaimedAmount: serializeTokenSubunits(0n),
|
|
1700
|
+
fundingSlices: input.fundingSlices,
|
|
1701
|
+
reclaimedSlices: [],
|
|
1702
|
+
version: 1,
|
|
1703
|
+
createdAt: input.occurredAt,
|
|
1704
|
+
updatedAt: input.occurredAt
|
|
1705
|
+
};
|
|
1706
|
+
assertModuleAllowance(allowance);
|
|
1707
|
+
return allowance;
|
|
1708
|
+
}
|
|
1709
|
+
function fundModuleAllowance(allowance, input) {
|
|
1710
|
+
assertModuleAllowance(allowance);
|
|
1663
1711
|
economyAssert(
|
|
1664
|
-
|
|
1712
|
+
allowance.status === "active" && allowance.version === input.expectedVersion,
|
|
1665
1713
|
"INVALID_CONTRACT",
|
|
1666
|
-
"
|
|
1714
|
+
"Module Allowance is closed or its optimistic version is stale"
|
|
1667
1715
|
);
|
|
1716
|
+
assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Funding time");
|
|
1717
|
+
const amount = assertWholePositive2(input.amount, "Module Allowance funding");
|
|
1718
|
+
assertPositiveSlices(input.fundingSlices, amount, "Funding slices");
|
|
1719
|
+
const next = {
|
|
1720
|
+
...allowance,
|
|
1721
|
+
allocatedAmount: serializeTokenSubunits(
|
|
1722
|
+
parseTokenSubunits(allowance.allocatedAmount) + amount
|
|
1723
|
+
),
|
|
1724
|
+
availableAmount: serializeTokenSubunits(
|
|
1725
|
+
parseTokenSubunits(allowance.availableAmount) + amount
|
|
1726
|
+
),
|
|
1727
|
+
fundingSlices: [...allowance.fundingSlices, ...input.fundingSlices],
|
|
1728
|
+
version: allowance.version + 1,
|
|
1729
|
+
updatedAt: input.occurredAt
|
|
1730
|
+
};
|
|
1731
|
+
assertModuleAllowance(next);
|
|
1732
|
+
return next;
|
|
1733
|
+
}
|
|
1734
|
+
function reclaimModuleAllowance(allowance, input) {
|
|
1735
|
+
assertModuleAllowance(allowance);
|
|
1668
1736
|
economyAssert(
|
|
1669
|
-
|
|
1737
|
+
allowance.status === "active" && allowance.version === input.expectedVersion,
|
|
1670
1738
|
"INVALID_CONTRACT",
|
|
1671
|
-
"
|
|
1739
|
+
"Module Allowance is closed or its optimistic version is stale"
|
|
1672
1740
|
);
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
"
|
|
1741
|
+
assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Reclaim time");
|
|
1742
|
+
const amount = assertWholePositive2(input.amount, "Module Allowance reclaim");
|
|
1743
|
+
economyAssert(
|
|
1744
|
+
amount <= parseTokenSubunits(allowance.availableAmount),
|
|
1745
|
+
"INSUFFICIENT_BALANCE",
|
|
1746
|
+
"Cannot reclaim held, spent, or unavailable Module Allowance value"
|
|
1747
|
+
);
|
|
1748
|
+
assertPositiveSlices(input.sourceSlices, amount, "Reclaim slices");
|
|
1749
|
+
const fundedLotIds = new Set(
|
|
1750
|
+
allowance.fundingSlices.map((slice) => slice.lotId)
|
|
1751
|
+
);
|
|
1752
|
+
economyAssert(
|
|
1753
|
+
input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
|
|
1754
|
+
"SOURCE_LOT_RESTRICTED",
|
|
1755
|
+
"Reclaim slices must originate from the Module Allowance"
|
|
1678
1756
|
);
|
|
1757
|
+
const next = {
|
|
1758
|
+
...allowance,
|
|
1759
|
+
availableAmount: serializeTokenSubunits(
|
|
1760
|
+
parseTokenSubunits(allowance.availableAmount) - amount
|
|
1761
|
+
),
|
|
1762
|
+
reclaimedAmount: serializeTokenSubunits(
|
|
1763
|
+
parseTokenSubunits(allowance.reclaimedAmount) + amount
|
|
1764
|
+
),
|
|
1765
|
+
reclaimedSlices: [...allowance.reclaimedSlices, ...input.sourceSlices],
|
|
1766
|
+
version: allowance.version + 1,
|
|
1767
|
+
updatedAt: input.occurredAt
|
|
1768
|
+
};
|
|
1769
|
+
assertModuleAllowance(next);
|
|
1770
|
+
return next;
|
|
1679
1771
|
}
|
|
1680
|
-
function
|
|
1772
|
+
function closeModuleAllowance(allowance, input) {
|
|
1773
|
+
assertModuleAllowance(allowance);
|
|
1681
1774
|
economyAssert(
|
|
1682
|
-
|
|
1775
|
+
allowance.status === "active" && allowance.version === input.expectedVersion,
|
|
1683
1776
|
"INVALID_CONTRACT",
|
|
1684
|
-
"
|
|
1777
|
+
"Module Allowance is closed or its optimistic version is stale"
|
|
1685
1778
|
);
|
|
1686
|
-
|
|
1687
|
-
assertEconomyIdentifier(event.commandId, "commandId");
|
|
1779
|
+
assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Closure time");
|
|
1688
1780
|
economyAssert(
|
|
1689
|
-
|
|
1781
|
+
parseTokenSubunits(allowance.availableAmount) === 0n && parseTokenSubunits(allowance.heldAmount) === 0n,
|
|
1690
1782
|
"INVALID_CONTRACT",
|
|
1691
|
-
"
|
|
1783
|
+
"Available value must be reclaimed and holds resolved before closure"
|
|
1692
1784
|
);
|
|
1785
|
+
const next = {
|
|
1786
|
+
...allowance,
|
|
1787
|
+
status: "closed",
|
|
1788
|
+
version: allowance.version + 1,
|
|
1789
|
+
updatedAt: input.occurredAt
|
|
1790
|
+
};
|
|
1791
|
+
assertModuleAllowance(next);
|
|
1792
|
+
return next;
|
|
1793
|
+
}
|
|
1794
|
+
function assertModuleSpendQuote(quote) {
|
|
1693
1795
|
economyAssert(
|
|
1694
|
-
|
|
1796
|
+
quote.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1695
1797
|
"INVALID_CONTRACT",
|
|
1696
|
-
"
|
|
1798
|
+
"Unsupported module-spend quote contract version"
|
|
1799
|
+
);
|
|
1800
|
+
assertEconomyIdentifier(quote.quoteId, "quoteId");
|
|
1801
|
+
assertEconomyIdentifier(quote.allowanceId, "allowanceId");
|
|
1802
|
+
assertEconomyIdentifier(quote.householdId, "householdId");
|
|
1803
|
+
assertEconomyIdentifier(quote.guardianAccountId, "guardianAccountId");
|
|
1804
|
+
assertEconomyIdentifier(quote.childAccountId, "childAccountId");
|
|
1805
|
+
assertEconomyIdentifier(quote.moduleVersionId, "moduleVersionId");
|
|
1806
|
+
assertEconomyIdentifier(quote.catalogVersion, "catalogVersion");
|
|
1807
|
+
assertEconomyIdentifier(
|
|
1808
|
+
quote.requirementsManifestVersion,
|
|
1809
|
+
"requirementsManifestVersion"
|
|
1810
|
+
);
|
|
1811
|
+
assertSha256Reference(
|
|
1812
|
+
quote.requirementsManifestHash,
|
|
1813
|
+
"Requirements manifest hash"
|
|
1814
|
+
);
|
|
1815
|
+
assertWholePositive2(quote.amount, "Module price");
|
|
1816
|
+
const acknowledgedAt = parseIsoTimestamp(quote.requirementsAcknowledgedAt);
|
|
1817
|
+
const issuedAt = parseIsoTimestamp(quote.issuedAt);
|
|
1818
|
+
const expiresAt = parseIsoTimestamp(quote.expiresAt);
|
|
1819
|
+
economyAssert(
|
|
1820
|
+
acknowledgedAt <= issuedAt && expiresAt > issuedAt,
|
|
1821
|
+
"INVALID_TIME_WINDOW",
|
|
1822
|
+
"Requirements must be acknowledged before a bounded quote is issued"
|
|
1697
1823
|
);
|
|
1698
|
-
if (event.failureCode !== void 0) {
|
|
1699
|
-
assertEconomyIdentifier(event.failureCode, "failureCode");
|
|
1700
|
-
}
|
|
1701
|
-
parseIsoTimestamp(event.occurredAt);
|
|
1702
1824
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1825
|
+
function createModuleSpendQuote(input) {
|
|
1826
|
+
const quote = {
|
|
1827
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
1828
|
+
...input
|
|
1829
|
+
};
|
|
1830
|
+
assertModuleSpendQuote(quote);
|
|
1831
|
+
return quote;
|
|
1832
|
+
}
|
|
1833
|
+
function assertModuleSpendHold(hold) {
|
|
1706
1834
|
economyAssert(
|
|
1707
|
-
|
|
1835
|
+
hold.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1708
1836
|
"INVALID_CONTRACT",
|
|
1709
|
-
"
|
|
1837
|
+
"Unsupported module-spend hold contract version"
|
|
1838
|
+
);
|
|
1839
|
+
assertEconomyIdentifier(hold.holdId, "holdId");
|
|
1840
|
+
assertEconomyIdentifier(hold.quoteId, "quoteId");
|
|
1841
|
+
assertEconomyIdentifier(hold.allowanceId, "allowanceId");
|
|
1842
|
+
assertEconomyIdentifier(hold.householdId, "householdId");
|
|
1843
|
+
assertEconomyIdentifier(hold.guardianAccountId, "guardianAccountId");
|
|
1844
|
+
assertEconomyIdentifier(hold.childAccountId, "childAccountId");
|
|
1845
|
+
assertEconomyIdentifier(hold.moduleVersionId, "moduleVersionId");
|
|
1846
|
+
assertEconomyIdentifier(
|
|
1847
|
+
hold.requirementsManifestVersion,
|
|
1848
|
+
"requirementsManifestVersion"
|
|
1710
1849
|
);
|
|
1711
|
-
|
|
1850
|
+
assertSha256Reference(
|
|
1851
|
+
hold.requirementsManifestHash,
|
|
1852
|
+
"Requirements manifest hash"
|
|
1853
|
+
);
|
|
1854
|
+
assertEconomyIdentifier(hold.holdIdempotencyKey, "holdIdempotencyKey");
|
|
1855
|
+
const amount = assertWholePositive2(hold.amount, "Module hold amount");
|
|
1856
|
+
assertPositiveSlices(hold.sourceSlices, amount, "Hold source slices");
|
|
1857
|
+
economyAssert(
|
|
1858
|
+
["held", "settled", "released"].includes(hold.status),
|
|
1859
|
+
"INVALID_CONTRACT",
|
|
1860
|
+
"Module hold status is unsupported"
|
|
1861
|
+
);
|
|
1862
|
+
economyAssert(
|
|
1863
|
+
Number.isSafeInteger(hold.version) && hold.version >= 1,
|
|
1864
|
+
"INVALID_CONTRACT",
|
|
1865
|
+
"Module hold version must be a positive safe integer"
|
|
1866
|
+
);
|
|
1867
|
+
const createdAt = parseIsoTimestamp(hold.createdAt);
|
|
1868
|
+
const expiresAt = parseIsoTimestamp(hold.expiresAt);
|
|
1869
|
+
const updatedAt = parseIsoTimestamp(hold.updatedAt);
|
|
1870
|
+
economyAssert(
|
|
1871
|
+
expiresAt > createdAt && updatedAt >= createdAt,
|
|
1872
|
+
"INVALID_TIME_WINDOW",
|
|
1873
|
+
"Module hold requires a bounded lifetime and ordered updates"
|
|
1874
|
+
);
|
|
1875
|
+
if (hold.status === "held") {
|
|
1712
1876
|
economyAssert(
|
|
1713
|
-
|
|
1877
|
+
hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.releaseTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseIdempotencyKey === void 0,
|
|
1714
1878
|
"INVALID_CONTRACT",
|
|
1715
|
-
"
|
|
1879
|
+
"An unsettled hold cannot claim an entitlement or final transaction"
|
|
1880
|
+
);
|
|
1881
|
+
} else if (hold.status === "settled") {
|
|
1882
|
+
economyAssert(
|
|
1883
|
+
hold.entitlementId !== void 0 && hold.settlementTransactionId !== void 0 && hold.settlementIdempotencyKey !== void 0 && hold.releaseTransactionId === void 0 && hold.releaseIdempotencyKey === void 0,
|
|
1884
|
+
"INVALID_CONTRACT",
|
|
1885
|
+
"A settled hold requires entitlement and transaction evidence"
|
|
1886
|
+
);
|
|
1887
|
+
assertEconomyIdentifier(hold.entitlementId, "entitlementId");
|
|
1888
|
+
assertEconomyIdentifier(
|
|
1889
|
+
hold.settlementTransactionId,
|
|
1890
|
+
"settlementTransactionId"
|
|
1891
|
+
);
|
|
1892
|
+
assertEconomyIdentifier(
|
|
1893
|
+
hold.settlementIdempotencyKey,
|
|
1894
|
+
"settlementIdempotencyKey"
|
|
1716
1895
|
);
|
|
1717
1896
|
} else {
|
|
1718
|
-
assertEconomyIdentifier(owner.ownerId, "ownerId");
|
|
1719
1897
|
economyAssert(
|
|
1720
|
-
|
|
1898
|
+
hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseTransactionId !== void 0 && hold.releaseIdempotencyKey !== void 0,
|
|
1721
1899
|
"INVALID_CONTRACT",
|
|
1722
|
-
"
|
|
1900
|
+
"A released hold requires release evidence only"
|
|
1723
1901
|
);
|
|
1902
|
+
assertEconomyIdentifier(hold.releaseTransactionId, "releaseTransactionId");
|
|
1903
|
+
assertEconomyIdentifier(hold.releaseIdempotencyKey, "releaseIdempotencyKey");
|
|
1724
1904
|
}
|
|
1725
1905
|
}
|
|
1726
|
-
function
|
|
1906
|
+
function assertAllowanceMatchesQuote(allowance, quote) {
|
|
1727
1907
|
economyAssert(
|
|
1728
|
-
|
|
1908
|
+
allowance.status === "active" && allowance.allowanceId === quote.allowanceId && allowance.householdId === quote.householdId && allowance.childAccountId === quote.childAccountId,
|
|
1729
1909
|
"INVALID_CONTRACT",
|
|
1730
|
-
"
|
|
1910
|
+
"Module quote does not belong to this active child allowance"
|
|
1731
1911
|
);
|
|
1732
|
-
|
|
1733
|
-
|
|
1912
|
+
}
|
|
1913
|
+
function assertAllowanceMatchesHold(allowance, hold) {
|
|
1734
1914
|
economyAssert(
|
|
1735
|
-
|
|
1736
|
-
"household-treasury",
|
|
1737
|
-
"personal",
|
|
1738
|
-
"gameplay-allocation",
|
|
1739
|
-
"hold",
|
|
1740
|
-
"purchase-clearing",
|
|
1741
|
-
"provider-clearing",
|
|
1742
|
-
"system"
|
|
1743
|
-
].includes(wallet.kind),
|
|
1915
|
+
allowance.status === "active" && allowance.allowanceId === hold.allowanceId && allowance.householdId === hold.householdId && allowance.childAccountId === hold.childAccountId,
|
|
1744
1916
|
"INVALID_CONTRACT",
|
|
1745
|
-
"
|
|
1917
|
+
"Module hold does not belong to this active child allowance"
|
|
1746
1918
|
);
|
|
1919
|
+
}
|
|
1920
|
+
function createModuleSpendHold(allowance, quote, input) {
|
|
1921
|
+
assertModuleAllowance(allowance);
|
|
1922
|
+
assertModuleSpendQuote(quote);
|
|
1923
|
+
assertAllowanceMatchesQuote(allowance, quote);
|
|
1747
1924
|
economyAssert(
|
|
1748
|
-
|
|
1925
|
+
allowance.version === input.expectedAllowanceVersion,
|
|
1749
1926
|
"INVALID_CONTRACT",
|
|
1750
|
-
"
|
|
1927
|
+
"Module Allowance optimistic version is stale"
|
|
1751
1928
|
);
|
|
1929
|
+
const occurredAt = parseIsoTimestamp(input.occurredAt);
|
|
1752
1930
|
economyAssert(
|
|
1753
|
-
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1931
|
+
occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(quote.issuedAt) && occurredAt < parseIsoTimestamp(quote.expiresAt),
|
|
1932
|
+
"INVALID_TIME_WINDOW",
|
|
1933
|
+
"Module quote is not valid at hold time"
|
|
1756
1934
|
);
|
|
1757
|
-
|
|
1758
|
-
economyAssert(
|
|
1759
|
-
wallet.ownerId === "system",
|
|
1760
|
-
"INVALID_CONTRACT",
|
|
1761
|
-
"System wallets must be owned by the system"
|
|
1762
|
-
);
|
|
1763
|
-
} else {
|
|
1764
|
-
assertEconomyIdentifier(wallet.ownerId, "ownerId");
|
|
1765
|
-
}
|
|
1766
|
-
if (wallet.householdId !== void 0) {
|
|
1767
|
-
assertEconomyIdentifier(wallet.householdId, "householdId");
|
|
1768
|
-
}
|
|
1769
|
-
if (wallet.ownerType === "household") {
|
|
1770
|
-
economyAssert(
|
|
1771
|
-
wallet.householdId === wallet.ownerId,
|
|
1772
|
-
"INVALID_CONTRACT",
|
|
1773
|
-
"Household wallets must identify the same household owner"
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
if (wallet.kind === "household-treasury") {
|
|
1777
|
-
economyAssert(
|
|
1778
|
-
wallet.ownerType === "household" && wallet.householdId !== void 0,
|
|
1779
|
-
"INVALID_CONTRACT",
|
|
1780
|
-
"Household treasuries must be owned by their household"
|
|
1781
|
-
);
|
|
1782
|
-
}
|
|
1783
|
-
if (wallet.kind === "personal" || wallet.kind === "gameplay-allocation") {
|
|
1784
|
-
economyAssert(
|
|
1785
|
-
wallet.ownerType === "account",
|
|
1786
|
-
"INVALID_CONTRACT",
|
|
1787
|
-
"Personal and gameplay-allocation wallets must be account-owned"
|
|
1788
|
-
);
|
|
1789
|
-
}
|
|
1790
|
-
if (wallet.kind === "gameplay-allocation") {
|
|
1791
|
-
economyAssert(
|
|
1792
|
-
wallet.householdId !== void 0,
|
|
1793
|
-
"INVALID_CONTRACT",
|
|
1794
|
-
"Gameplay-allocation wallets must retain household provenance"
|
|
1795
|
-
);
|
|
1796
|
-
}
|
|
1797
|
-
if (wallet.kind === "purchase-clearing" || wallet.kind === "provider-clearing" || wallet.kind === "system") {
|
|
1798
|
-
economyAssert(
|
|
1799
|
-
wallet.ownerType === "system" && wallet.householdId === void 0,
|
|
1800
|
-
"INVALID_CONTRACT",
|
|
1801
|
-
"Clearing and system wallets must be system-owned"
|
|
1802
|
-
);
|
|
1803
|
-
}
|
|
1935
|
+
const amount = parseTokenSubunits(quote.amount);
|
|
1804
1936
|
economyAssert(
|
|
1805
|
-
|
|
1937
|
+
amount <= parseTokenSubunits(allowance.availableAmount),
|
|
1938
|
+
"INSUFFICIENT_BALANCE",
|
|
1939
|
+
"Module Allowance has insufficient available value"
|
|
1940
|
+
);
|
|
1941
|
+
assertEconomyIdentifier(input.holdId, "holdId");
|
|
1942
|
+
assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
|
|
1943
|
+
assertPositiveSlices(input.sourceSlices, amount, "Hold source slices");
|
|
1944
|
+
const fundedLotIds = new Set(
|
|
1945
|
+
allowance.fundingSlices.map((slice) => slice.lotId)
|
|
1946
|
+
);
|
|
1947
|
+
economyAssert(
|
|
1948
|
+
input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
|
|
1949
|
+
"SOURCE_LOT_RESTRICTED",
|
|
1950
|
+
"Hold slices must originate from the Module Allowance"
|
|
1951
|
+
);
|
|
1952
|
+
const nextAllowance = {
|
|
1953
|
+
...allowance,
|
|
1954
|
+
availableAmount: serializeTokenSubunits(
|
|
1955
|
+
parseTokenSubunits(allowance.availableAmount) - amount
|
|
1956
|
+
),
|
|
1957
|
+
heldAmount: serializeTokenSubunits(
|
|
1958
|
+
parseTokenSubunits(allowance.heldAmount) + amount
|
|
1959
|
+
),
|
|
1960
|
+
version: allowance.version + 1,
|
|
1961
|
+
updatedAt: input.occurredAt
|
|
1962
|
+
};
|
|
1963
|
+
const hold = {
|
|
1964
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
1965
|
+
holdId: input.holdId,
|
|
1966
|
+
quoteId: quote.quoteId,
|
|
1967
|
+
allowanceId: quote.allowanceId,
|
|
1968
|
+
householdId: quote.householdId,
|
|
1969
|
+
guardianAccountId: quote.guardianAccountId,
|
|
1970
|
+
childAccountId: quote.childAccountId,
|
|
1971
|
+
moduleVersionId: quote.moduleVersionId,
|
|
1972
|
+
amount: quote.amount,
|
|
1973
|
+
requirementsManifestVersion: quote.requirementsManifestVersion,
|
|
1974
|
+
requirementsManifestHash: quote.requirementsManifestHash,
|
|
1975
|
+
sourceSlices: input.sourceSlices,
|
|
1976
|
+
status: "held",
|
|
1977
|
+
holdIdempotencyKey: input.idempotencyKey,
|
|
1978
|
+
version: 1,
|
|
1979
|
+
createdAt: input.occurredAt,
|
|
1980
|
+
expiresAt: quote.expiresAt,
|
|
1981
|
+
updatedAt: input.occurredAt
|
|
1982
|
+
};
|
|
1983
|
+
assertModuleAllowance(nextAllowance);
|
|
1984
|
+
assertModuleSpendHold(hold);
|
|
1985
|
+
return { allowance: nextAllowance, hold };
|
|
1986
|
+
}
|
|
1987
|
+
function settleModuleSpendHold(allowance, hold, input) {
|
|
1988
|
+
assertModuleAllowance(allowance);
|
|
1989
|
+
assertModuleSpendHold(hold);
|
|
1990
|
+
assertAllowanceMatchesHold(allowance, hold);
|
|
1991
|
+
economyAssert(
|
|
1992
|
+
hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
|
|
1806
1993
|
"INVALID_CONTRACT",
|
|
1807
|
-
"
|
|
1994
|
+
"Module hold is final or an optimistic version is stale"
|
|
1808
1995
|
);
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1996
|
+
assertEconomyIdentifier(input.entitlementId, "entitlementId");
|
|
1997
|
+
assertEconomyIdentifier(
|
|
1998
|
+
input.settlementTransactionId,
|
|
1999
|
+
"settlementTransactionId"
|
|
2000
|
+
);
|
|
2001
|
+
assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
|
|
2002
|
+
const occurredAt = parseIsoTimestamp(input.occurredAt);
|
|
2003
|
+
economyAssert(
|
|
2004
|
+
occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(hold.updatedAt) && occurredAt < parseIsoTimestamp(hold.expiresAt),
|
|
2005
|
+
"INVALID_TIME_WINDOW",
|
|
2006
|
+
"Module hold cannot settle outside its valid lifetime"
|
|
2007
|
+
);
|
|
2008
|
+
const amount = parseTokenSubunits(hold.amount);
|
|
2009
|
+
economyAssert(
|
|
2010
|
+
amount <= parseTokenSubunits(allowance.heldAmount),
|
|
2011
|
+
"INSUFFICIENT_BALANCE",
|
|
2012
|
+
"Module Allowance does not contain the quoted held value"
|
|
2013
|
+
);
|
|
2014
|
+
const nextAllowance = {
|
|
2015
|
+
...allowance,
|
|
2016
|
+
heldAmount: serializeTokenSubunits(
|
|
2017
|
+
parseTokenSubunits(allowance.heldAmount) - amount
|
|
2018
|
+
),
|
|
2019
|
+
spentAmount: serializeTokenSubunits(
|
|
2020
|
+
parseTokenSubunits(allowance.spentAmount) + amount
|
|
2021
|
+
),
|
|
2022
|
+
version: allowance.version + 1,
|
|
2023
|
+
updatedAt: input.occurredAt
|
|
2024
|
+
};
|
|
2025
|
+
const nextHold = {
|
|
2026
|
+
...hold,
|
|
2027
|
+
status: "settled",
|
|
2028
|
+
settlementIdempotencyKey: input.idempotencyKey,
|
|
2029
|
+
entitlementId: input.entitlementId,
|
|
2030
|
+
settlementTransactionId: input.settlementTransactionId,
|
|
2031
|
+
version: hold.version + 1,
|
|
2032
|
+
updatedAt: input.occurredAt
|
|
2033
|
+
};
|
|
2034
|
+
assertModuleAllowance(nextAllowance);
|
|
2035
|
+
assertModuleSpendHold(nextHold);
|
|
2036
|
+
return { allowance: nextAllowance, hold: nextHold };
|
|
1823
2037
|
}
|
|
1824
|
-
function
|
|
2038
|
+
function releaseModuleSpendHold(allowance, hold, input) {
|
|
2039
|
+
assertModuleAllowance(allowance);
|
|
2040
|
+
assertModuleSpendHold(hold);
|
|
2041
|
+
assertAllowanceMatchesHold(allowance, hold);
|
|
1825
2042
|
economyAssert(
|
|
1826
|
-
|
|
2043
|
+
hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
|
|
1827
2044
|
"INVALID_CONTRACT",
|
|
1828
|
-
"
|
|
2045
|
+
"Module hold is final or an optimistic version is stale"
|
|
1829
2046
|
);
|
|
1830
|
-
assertEconomyIdentifier(
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
summary.reserved,
|
|
1834
|
-
summary.held,
|
|
1835
|
-
summary.rewardProgress
|
|
1836
|
-
]) {
|
|
1837
|
-
economyAssert(
|
|
1838
|
-
parseTokenSubunits(amount) >= 0n,
|
|
1839
|
-
"INVALID_AMOUNT",
|
|
1840
|
-
"Wallet summary categories cannot be negative"
|
|
1841
|
-
);
|
|
1842
|
-
}
|
|
2047
|
+
assertEconomyIdentifier(input.releaseTransactionId, "releaseTransactionId");
|
|
2048
|
+
assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
|
|
2049
|
+
const occurredAt = parseIsoTimestamp(input.occurredAt);
|
|
1843
2050
|
economyAssert(
|
|
1844
|
-
|
|
2051
|
+
occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(hold.updatedAt),
|
|
2052
|
+
"INVALID_TIME_WINDOW",
|
|
2053
|
+
"Module hold release cannot precede its current state"
|
|
2054
|
+
);
|
|
2055
|
+
const amount = parseTokenSubunits(hold.amount);
|
|
2056
|
+
economyAssert(
|
|
2057
|
+
amount <= parseTokenSubunits(allowance.heldAmount),
|
|
2058
|
+
"INSUFFICIENT_BALANCE",
|
|
2059
|
+
"Module Allowance does not contain the quoted held value"
|
|
2060
|
+
);
|
|
2061
|
+
const nextAllowance = {
|
|
2062
|
+
...allowance,
|
|
2063
|
+
availableAmount: serializeTokenSubunits(
|
|
2064
|
+
parseTokenSubunits(allowance.availableAmount) + amount
|
|
2065
|
+
),
|
|
2066
|
+
heldAmount: serializeTokenSubunits(
|
|
2067
|
+
parseTokenSubunits(allowance.heldAmount) - amount
|
|
2068
|
+
),
|
|
2069
|
+
version: allowance.version + 1,
|
|
2070
|
+
updatedAt: input.occurredAt
|
|
2071
|
+
};
|
|
2072
|
+
const nextHold = {
|
|
2073
|
+
...hold,
|
|
2074
|
+
status: "released",
|
|
2075
|
+
releaseIdempotencyKey: input.idempotencyKey,
|
|
2076
|
+
releaseTransactionId: input.releaseTransactionId,
|
|
2077
|
+
version: hold.version + 1,
|
|
2078
|
+
updatedAt: input.occurredAt
|
|
2079
|
+
};
|
|
2080
|
+
assertModuleAllowance(nextAllowance);
|
|
2081
|
+
assertModuleSpendHold(nextHold);
|
|
2082
|
+
return { allowance: nextAllowance, hold: nextHold };
|
|
2083
|
+
}
|
|
2084
|
+
function assertModulePurchaseReceipt(receipt) {
|
|
2085
|
+
economyAssert(
|
|
2086
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1845
2087
|
"INVALID_CONTRACT",
|
|
1846
|
-
"
|
|
2088
|
+
"Unsupported module-purchase receipt contract version"
|
|
2089
|
+
);
|
|
2090
|
+
assertEconomyIdentifier(receipt.receiptId, "receiptId");
|
|
2091
|
+
assertEconomyIdentifier(receipt.quoteId, "quoteId");
|
|
2092
|
+
assertEconomyIdentifier(receipt.holdId, "holdId");
|
|
2093
|
+
assertEconomyIdentifier(receipt.allowanceId, "allowanceId");
|
|
2094
|
+
assertEconomyIdentifier(receipt.householdId, "householdId");
|
|
2095
|
+
assertEconomyIdentifier(receipt.guardianAccountId, "guardianAccountId");
|
|
2096
|
+
assertEconomyIdentifier(receipt.childAccountId, "childAccountId");
|
|
2097
|
+
assertEconomyIdentifier(receipt.moduleVersionId, "moduleVersionId");
|
|
2098
|
+
assertEconomyIdentifier(receipt.entitlementId, "entitlementId");
|
|
2099
|
+
assertEconomyIdentifier(
|
|
2100
|
+
receipt.requirementsManifestVersion,
|
|
2101
|
+
"requirementsManifestVersion"
|
|
1847
2102
|
);
|
|
1848
|
-
|
|
2103
|
+
assertSha256Reference(
|
|
2104
|
+
receipt.requirementsManifestHash,
|
|
2105
|
+
"Requirements manifest hash"
|
|
2106
|
+
);
|
|
2107
|
+
assertEconomyIdentifier(
|
|
2108
|
+
receipt.settlementTransactionId,
|
|
2109
|
+
"settlementTransactionId"
|
|
2110
|
+
);
|
|
2111
|
+
assertWholePositive2(receipt.amount, "Receipt amount");
|
|
2112
|
+
parseIsoTimestamp(receipt.issuedAt);
|
|
1849
2113
|
}
|
|
1850
|
-
function
|
|
2114
|
+
function createModulePurchaseReceipt(quote, hold, input) {
|
|
2115
|
+
assertModuleSpendQuote(quote);
|
|
2116
|
+
assertModuleSpendHold(hold);
|
|
1851
2117
|
economyAssert(
|
|
1852
|
-
|
|
2118
|
+
hold.status === "settled" && hold.quoteId === quote.quoteId && hold.allowanceId === quote.allowanceId && hold.householdId === quote.householdId && hold.guardianAccountId === quote.guardianAccountId && hold.childAccountId === quote.childAccountId && hold.moduleVersionId === quote.moduleVersionId && hold.amount === quote.amount && hold.requirementsManifestVersion === quote.requirementsManifestVersion && hold.requirementsManifestHash === quote.requirementsManifestHash,
|
|
1853
2119
|
"INVALID_CONTRACT",
|
|
1854
|
-
"
|
|
2120
|
+
"Receipt quote and settled hold evidence do not match"
|
|
1855
2121
|
);
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2122
|
+
economyAssert(
|
|
2123
|
+
parseIsoTimestamp(input.issuedAt) >= parseIsoTimestamp(hold.updatedAt),
|
|
2124
|
+
"INVALID_TIME_WINDOW",
|
|
2125
|
+
"Receipt cannot precede settlement"
|
|
2126
|
+
);
|
|
2127
|
+
const receipt = {
|
|
2128
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2129
|
+
receiptId: input.receiptId,
|
|
2130
|
+
quoteId: quote.quoteId,
|
|
2131
|
+
holdId: hold.holdId,
|
|
2132
|
+
allowanceId: quote.allowanceId,
|
|
2133
|
+
householdId: quote.householdId,
|
|
2134
|
+
guardianAccountId: quote.guardianAccountId,
|
|
2135
|
+
childAccountId: quote.childAccountId,
|
|
2136
|
+
moduleVersionId: quote.moduleVersionId,
|
|
2137
|
+
entitlementId: hold.entitlementId,
|
|
2138
|
+
amount: quote.amount,
|
|
2139
|
+
requirementsManifestVersion: quote.requirementsManifestVersion,
|
|
2140
|
+
requirementsManifestHash: quote.requirementsManifestHash,
|
|
2141
|
+
settlementTransactionId: hold.settlementTransactionId,
|
|
2142
|
+
issuedAt: input.issuedAt
|
|
2143
|
+
};
|
|
2144
|
+
assertModulePurchaseReceipt(receipt);
|
|
2145
|
+
return receipt;
|
|
1870
2146
|
}
|
|
1871
|
-
|
|
1872
|
-
// src/projection.ts
|
|
1873
|
-
function assertWalletBalanceProjection(projection) {
|
|
2147
|
+
function assertModulePurchaseReconciliationObservation(observation) {
|
|
1874
2148
|
economyAssert(
|
|
1875
|
-
|
|
2149
|
+
observation.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1876
2150
|
"INVALID_CONTRACT",
|
|
1877
|
-
"Unsupported
|
|
2151
|
+
"Unsupported module-purchase reconciliation contract version"
|
|
1878
2152
|
);
|
|
1879
|
-
assertEconomyIdentifier(
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
projection.held
|
|
1884
|
-
]) {
|
|
1885
|
-
economyAssert(
|
|
1886
|
-
parseTokenSubunits(amount) >= 0n,
|
|
1887
|
-
"NEGATIVE_PROJECTION",
|
|
1888
|
-
"Wallet projection buckets cannot be negative"
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
2153
|
+
assertEconomyIdentifier(observation.quoteId, "quoteId");
|
|
2154
|
+
assertEconomyIdentifier(observation.holdId, "holdId");
|
|
2155
|
+
assertEconomyIdentifier(observation.childAccountId, "childAccountId");
|
|
2156
|
+
assertEconomyIdentifier(observation.moduleVersionId, "moduleVersionId");
|
|
1891
2157
|
economyAssert(
|
|
1892
|
-
|
|
2158
|
+
["missing", "held", "settled", "released"].includes(
|
|
2159
|
+
observation.financialState
|
|
2160
|
+
),
|
|
1893
2161
|
"INVALID_CONTRACT",
|
|
1894
|
-
"
|
|
2162
|
+
"Reconciliation financial state is unsupported"
|
|
1895
2163
|
);
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2164
|
+
economyAssert(
|
|
2165
|
+
["missing", "pending", "active", "cancelled"].includes(
|
|
2166
|
+
observation.entitlementState
|
|
2167
|
+
),
|
|
2168
|
+
"INVALID_CONTRACT",
|
|
2169
|
+
"Reconciliation entitlement state is unsupported"
|
|
2170
|
+
);
|
|
2171
|
+
economyAssert(
|
|
2172
|
+
typeof observation.receiptPresent === "boolean",
|
|
2173
|
+
"INVALID_CONTRACT",
|
|
2174
|
+
"Receipt presence must be Boolean"
|
|
2175
|
+
);
|
|
2176
|
+
parseIsoTimestamp(observation.observedAt);
|
|
2177
|
+
}
|
|
2178
|
+
function reconciliationResult(action, reasonCode, blocking = false, consistent = false) {
|
|
2179
|
+
return {
|
|
2180
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2181
|
+
action,
|
|
2182
|
+
blocking,
|
|
2183
|
+
consistent,
|
|
2184
|
+
reasonCode
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
function reconcileModulePurchase(observation) {
|
|
2188
|
+
assertModulePurchaseReconciliationObservation(observation);
|
|
2189
|
+
const { financialState, entitlementState, receiptPresent } = observation;
|
|
2190
|
+
if (receiptPresent && !(financialState === "settled" && entitlementState === "active")) {
|
|
2191
|
+
return reconciliationResult(
|
|
2192
|
+
"manual-review",
|
|
2193
|
+
"receipt-conflicts-with-authoritative-state",
|
|
2194
|
+
true
|
|
1900
2195
|
);
|
|
1901
2196
|
}
|
|
1902
|
-
|
|
2197
|
+
if (financialState === "settled" && entitlementState === "active") {
|
|
2198
|
+
return receiptPresent ? reconciliationResult("none", "purchase-consistent", false, true) : reconciliationResult("issue-receipt", "settled-active-receipt-missing");
|
|
2199
|
+
}
|
|
2200
|
+
if (financialState === "held" && entitlementState === "pending") {
|
|
2201
|
+
return reconciliationResult("resume-settlement", "held-pending");
|
|
2202
|
+
}
|
|
2203
|
+
if (financialState === "held" && (entitlementState === "missing" || entitlementState === "cancelled")) {
|
|
2204
|
+
return reconciliationResult("release-hold", "held-without-usable-entitlement");
|
|
2205
|
+
}
|
|
2206
|
+
if (financialState === "settled" && entitlementState === "pending") {
|
|
2207
|
+
return reconciliationResult("activate-entitlement", "settled-pending");
|
|
2208
|
+
}
|
|
2209
|
+
if ((financialState === "missing" || financialState === "released") && entitlementState === "pending") {
|
|
2210
|
+
return reconciliationResult(
|
|
2211
|
+
"cancel-pending-entitlement",
|
|
2212
|
+
"pending-without-settlement-path"
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
if ((financialState === "missing" || financialState === "released") && (entitlementState === "missing" || entitlementState === "cancelled")) {
|
|
2216
|
+
return reconciliationResult("none", "no-active-purchase", false, true);
|
|
2217
|
+
}
|
|
2218
|
+
return reconciliationResult(
|
|
2219
|
+
"manual-review",
|
|
2220
|
+
financialState === "settled" ? "settled-debit-without-entitlement" : "active-entitlement-without-settled-debit",
|
|
2221
|
+
true
|
|
2222
|
+
);
|
|
1903
2223
|
}
|
|
1904
|
-
|
|
2224
|
+
|
|
2225
|
+
// src/operations.ts
|
|
2226
|
+
function assertDistinctAdjustmentApproval(request) {
|
|
1905
2227
|
economyAssert(
|
|
1906
|
-
|
|
2228
|
+
request.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1907
2229
|
"INVALID_CONTRACT",
|
|
1908
|
-
"Unsupported
|
|
2230
|
+
"Unsupported adjustment contract version"
|
|
1909
2231
|
);
|
|
1910
|
-
assertEconomyIdentifier(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
parseTokenSubunits(delta.reservedDelta),
|
|
1914
|
-
parseTokenSubunits(delta.heldDelta)
|
|
1915
|
-
];
|
|
2232
|
+
assertEconomyIdentifier(request.adjustmentId, "adjustmentId");
|
|
2233
|
+
assertEconomyIdentifier(request.walletId, "walletId");
|
|
2234
|
+
assertEconomyIdentifier(request.initiatedByAccountId, "initiatedByAccountId");
|
|
1916
2235
|
economyAssert(
|
|
1917
|
-
|
|
2236
|
+
parseTokenSubunits(request.amount) !== 0n,
|
|
1918
2237
|
"INVALID_AMOUNT",
|
|
1919
|
-
"
|
|
2238
|
+
"Adjustment amount cannot be zero"
|
|
1920
2239
|
);
|
|
1921
|
-
}
|
|
1922
|
-
function assertWalletBalanceDeltaBatch(transactionId, deltas) {
|
|
1923
|
-
assertEconomyIdentifier(transactionId, "transactionId");
|
|
1924
2240
|
economyAssert(
|
|
1925
|
-
|
|
2241
|
+
request.reason.trim().length > 0 && request.reason.length <= 512,
|
|
1926
2242
|
"INVALID_CONTRACT",
|
|
1927
|
-
"
|
|
2243
|
+
"Adjustment reason is required and must be bounded"
|
|
1928
2244
|
);
|
|
1929
|
-
|
|
1930
|
-
|
|
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");
|
|
2245
|
+
assertEconomyIdentifier(request.ticketReference, "ticketReference");
|
|
2246
|
+
const initiatedAt = parseIsoTimestamp(request.initiatedAt);
|
|
1944
2247
|
economyAssert(
|
|
1945
|
-
|
|
2248
|
+
request.approvedByAccountId !== void 0 && request.approvedByAccountId !== request.initiatedByAccountId,
|
|
1946
2249
|
"INVALID_CONTRACT",
|
|
1947
|
-
"
|
|
2250
|
+
"Adjustment approval requires a distinct authorized operator"
|
|
1948
2251
|
);
|
|
2252
|
+
assertEconomyIdentifier(request.approvedByAccountId, "approvedByAccountId");
|
|
1949
2253
|
economyAssert(
|
|
1950
|
-
parseIsoTimestamp(
|
|
2254
|
+
request.approvedAt !== void 0 && parseIsoTimestamp(request.approvedAt) >= initiatedAt,
|
|
1951
2255
|
"INVALID_TIME_WINDOW",
|
|
1952
|
-
"
|
|
2256
|
+
"Adjustment approval time must follow initiation"
|
|
1953
2257
|
);
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
version: projection.version + 1,
|
|
1966
|
-
lastTransactionId: transactionId,
|
|
1967
|
-
asOf
|
|
1968
|
-
};
|
|
1969
|
-
assertWalletBalanceProjection(next);
|
|
1970
|
-
return next;
|
|
2258
|
+
economyAssert(
|
|
2259
|
+
request.status === "approved" || request.status === "executed",
|
|
2260
|
+
"INVALID_CONTRACT",
|
|
2261
|
+
"Only approved adjustments satisfy dual-control approval"
|
|
2262
|
+
);
|
|
2263
|
+
if (request.resultingTransactionId !== void 0) {
|
|
2264
|
+
assertEconomyIdentifier(
|
|
2265
|
+
request.resultingTransactionId,
|
|
2266
|
+
"resultingTransactionId"
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
1971
2269
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2270
|
+
|
|
2271
|
+
// src/paid-lots.ts
|
|
2272
|
+
var ZERO_ARITHMETIC = Object.freeze({
|
|
2273
|
+
retainedDelta: serializeTokenSubunits(0n),
|
|
2274
|
+
heldDelta: serializeTokenSubunits(0n),
|
|
2275
|
+
reversedDelta: serializeTokenSubunits(0n),
|
|
2276
|
+
refundedDelta: serializeTokenSubunits(0n),
|
|
2277
|
+
chargebackDelta: serializeTokenSubunits(0n),
|
|
2278
|
+
oneTimeReversalDelta: serializeTokenSubunits(0n)
|
|
2279
|
+
});
|
|
2280
|
+
var EVENT_TYPES = [
|
|
2281
|
+
"refund",
|
|
2282
|
+
"dispute-hold",
|
|
2283
|
+
"dispute-won",
|
|
2284
|
+
"dispute-lost",
|
|
2285
|
+
"chargeback",
|
|
2286
|
+
"reversal"
|
|
2287
|
+
];
|
|
2288
|
+
var EVENT_PRECEDENCE = {
|
|
2289
|
+
"dispute-hold": 0,
|
|
2290
|
+
"dispute-won": 1,
|
|
2291
|
+
"dispute-lost": 1,
|
|
2292
|
+
refund: 2,
|
|
2293
|
+
chargeback: 3,
|
|
2294
|
+
reversal: 4
|
|
2295
|
+
};
|
|
2296
|
+
function assertPaidLotPurchaseProvenance(provenance) {
|
|
2297
|
+
economyAssert(
|
|
2298
|
+
provenance.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2299
|
+
"INVALID_CONTRACT",
|
|
2300
|
+
"Unsupported paid-lot provenance contract version"
|
|
2301
|
+
);
|
|
2302
|
+
assertEconomyIdentifier(provenance.purchaseId, "purchaseId");
|
|
2303
|
+
assertEconomyIdentifier(provenance.catalogVersion, "catalogVersion");
|
|
2304
|
+
parseIsoTimestamp(provenance.purchasedAt);
|
|
2305
|
+
}
|
|
2306
|
+
function assertPaidLotLifecycleEvent(event) {
|
|
2307
|
+
economyAssert(
|
|
2308
|
+
event.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2309
|
+
"INVALID_CONTRACT",
|
|
2310
|
+
"Unsupported paid-lot lifecycle event contract version"
|
|
2311
|
+
);
|
|
2312
|
+
assertEconomyIdentifier(event.eventId, "eventId");
|
|
2313
|
+
assertEconomyIdentifier(event.lotId, "lotId");
|
|
2314
|
+
economyAssert(
|
|
2315
|
+
EVENT_TYPES.includes(event.eventType),
|
|
2316
|
+
"INVALID_CONTRACT",
|
|
2317
|
+
"Paid-lot lifecycle event type is unsupported"
|
|
2318
|
+
);
|
|
2319
|
+
economyAssert(
|
|
2320
|
+
parseTokenSubunits(event.amount) > 0n,
|
|
2321
|
+
"INVALID_AMOUNT",
|
|
2322
|
+
"Paid-lot lifecycle event amount must be positive"
|
|
2323
|
+
);
|
|
2324
|
+
parseIsoTimestamp(event.occurredAt);
|
|
2325
|
+
}
|
|
2326
|
+
function sameEvent(left, right) {
|
|
2327
|
+
return left.schemaVersion === right.schemaVersion && left.eventId === right.eventId && left.lotId === right.lotId && left.eventType === right.eventType && left.amount === right.amount && left.occurredAt === right.occurredAt;
|
|
2328
|
+
}
|
|
2329
|
+
function compareEvents(left, right) {
|
|
2330
|
+
const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
|
|
2331
|
+
if (timeDifference !== 0) {
|
|
2332
|
+
return timeDifference;
|
|
2333
|
+
}
|
|
2334
|
+
const precedenceDifference = EVENT_PRECEDENCE[left.eventType] - EVENT_PRECEDENCE[right.eventType];
|
|
2335
|
+
return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.eventId, right.eventId) : precedenceDifference;
|
|
2336
|
+
}
|
|
2337
|
+
function deriveStatus(retained, held, reversed, chargeback) {
|
|
2338
|
+
if (held > 0n) {
|
|
2339
|
+
return "disputed";
|
|
2340
|
+
}
|
|
2341
|
+
if (chargeback > 0n) {
|
|
2342
|
+
return "chargeback-lost";
|
|
2343
|
+
}
|
|
2344
|
+
if (retained === 0n) {
|
|
2345
|
+
return "fully-reversed";
|
|
2346
|
+
}
|
|
2347
|
+
return reversed > 0n ? "partially-reversed" : "clear";
|
|
2348
|
+
}
|
|
2349
|
+
function immutableFactsFromLifecycle(lifecycle) {
|
|
2350
|
+
return {
|
|
2351
|
+
schemaVersion: lifecycle.schemaVersion,
|
|
2352
|
+
lotId: lifecycle.lotId,
|
|
2353
|
+
payerAccountId: lifecycle.payerAccountId,
|
|
2354
|
+
receivingHouseholdId: lifecycle.receivingHouseholdId,
|
|
2355
|
+
purchaseId: lifecycle.purchaseId,
|
|
2356
|
+
catalogVersion: lifecycle.catalogVersion,
|
|
2357
|
+
purchasedAt: lifecycle.purchasedAt,
|
|
2358
|
+
settledAt: lifecycle.settledAt,
|
|
2359
|
+
creditedAt: lifecycle.creditedAt,
|
|
2360
|
+
originalAmount: lifecycle.originalAmount
|
|
1985
2361
|
};
|
|
1986
|
-
assertDeterministicWalletBalanceSummary(summary);
|
|
1987
|
-
return summary;
|
|
1988
2362
|
}
|
|
1989
|
-
function
|
|
1990
|
-
assertWalletBalanceSummary(summary);
|
|
2363
|
+
function assertImmutableFacts(facts) {
|
|
1991
2364
|
economyAssert(
|
|
1992
|
-
|
|
2365
|
+
facts.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1993
2366
|
"INVALID_CONTRACT",
|
|
1994
|
-
"
|
|
2367
|
+
"Unsupported paid-lot lifecycle contract version"
|
|
2368
|
+
);
|
|
2369
|
+
assertEconomyIdentifier(facts.lotId, "lotId");
|
|
2370
|
+
assertEconomyIdentifier(facts.payerAccountId, "payerAccountId");
|
|
2371
|
+
assertEconomyIdentifier(
|
|
2372
|
+
facts.receivingHouseholdId,
|
|
2373
|
+
"receivingHouseholdId"
|
|
2374
|
+
);
|
|
2375
|
+
assertEconomyIdentifier(facts.purchaseId, "purchaseId");
|
|
2376
|
+
assertEconomyIdentifier(facts.catalogVersion, "catalogVersion");
|
|
2377
|
+
const purchasedAt = parseIsoTimestamp(facts.purchasedAt);
|
|
2378
|
+
const settledAt = parseIsoTimestamp(facts.settledAt);
|
|
2379
|
+
const creditedAt = parseIsoTimestamp(facts.creditedAt);
|
|
2380
|
+
economyAssert(
|
|
2381
|
+
purchasedAt <= settledAt && settledAt <= creditedAt,
|
|
2382
|
+
"INVALID_TIME_WINDOW",
|
|
2383
|
+
"Paid-lot timestamps must follow purchase, settlement, then credit order"
|
|
2384
|
+
);
|
|
2385
|
+
economyAssert(
|
|
2386
|
+
parseTokenSubunits(facts.originalAmount) > 0n,
|
|
2387
|
+
"INVALID_AMOUNT",
|
|
2388
|
+
"Paid-lot original amount must be positive"
|
|
1995
2389
|
);
|
|
1996
2390
|
}
|
|
1997
|
-
function
|
|
1998
|
-
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
assertEconomyIdentifier(binding.walletId, "walletId");
|
|
2391
|
+
function projectPaidLot(facts, receipts) {
|
|
2392
|
+
assertImmutableFacts(facts);
|
|
2393
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
2394
|
+
let reversalReceipts = 0;
|
|
2395
|
+
for (const receipt of receipts) {
|
|
2003
2396
|
economyAssert(
|
|
2004
|
-
|
|
2397
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2005
2398
|
"INVALID_CONTRACT",
|
|
2006
|
-
"
|
|
2399
|
+
"Unsupported paid-lot receipt contract version"
|
|
2007
2400
|
);
|
|
2401
|
+
assertPaidLotLifecycleEvent(receipt.event);
|
|
2008
2402
|
economyAssert(
|
|
2009
|
-
!
|
|
2403
|
+
receipt.event.lotId === facts.lotId && !eventIds.has(receipt.event.eventId),
|
|
2010
2404
|
"DUPLICATE_IDENTIFIER",
|
|
2011
|
-
"
|
|
2405
|
+
"Paid-lot receipts must be unique and belong to the lifecycle lot"
|
|
2012
2406
|
);
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
for (const posting of transaction.postings) {
|
|
2017
|
-
if (posting.walletId === void 0) {
|
|
2018
|
-
continue;
|
|
2407
|
+
eventIds.add(receipt.event.eventId);
|
|
2408
|
+
if (receipt.event.eventType === "reversal") {
|
|
2409
|
+
reversalReceipts += 1;
|
|
2019
2410
|
}
|
|
2020
|
-
|
|
2411
|
+
}
|
|
2412
|
+
economyAssert(
|
|
2413
|
+
reversalReceipts <= 1,
|
|
2414
|
+
"REVERSAL_ALREADY_EXISTS",
|
|
2415
|
+
"Paid-lot lifecycle permits only one distinct reversal event"
|
|
2416
|
+
);
|
|
2417
|
+
let retained = parseTokenSubunits(facts.originalAmount);
|
|
2418
|
+
let held = 0n;
|
|
2419
|
+
let reversed = 0n;
|
|
2420
|
+
let refunded = 0n;
|
|
2421
|
+
let chargeback = 0n;
|
|
2422
|
+
let oneTimeReversal = 0n;
|
|
2423
|
+
const effectiveEventIds = [];
|
|
2424
|
+
for (const receipt of [...receipts].sort(
|
|
2425
|
+
(left, right) => compareEvents(left.event, right.event)
|
|
2426
|
+
)) {
|
|
2427
|
+
const event = receipt.event;
|
|
2021
2428
|
economyAssert(
|
|
2022
|
-
|
|
2023
|
-
"
|
|
2024
|
-
"
|
|
2429
|
+
parseIsoTimestamp(event.occurredAt) >= parseIsoTimestamp(facts.creditedAt),
|
|
2430
|
+
"INVALID_TIME_WINDOW",
|
|
2431
|
+
"Paid-lot lifecycle event cannot precede credit"
|
|
2025
2432
|
);
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2433
|
+
const amount = parseTokenSubunits(event.amount);
|
|
2434
|
+
if (event.eventType === "dispute-hold") {
|
|
2435
|
+
economyAssert(
|
|
2436
|
+
amount <= retained - held,
|
|
2437
|
+
"INSUFFICIENT_ELIGIBLE_LOTS",
|
|
2438
|
+
"Dispute hold exceeds unheld retained paid-lot basis"
|
|
2439
|
+
);
|
|
2440
|
+
held += amount;
|
|
2441
|
+
} else if (event.eventType === "dispute-won") {
|
|
2442
|
+
economyAssert(
|
|
2443
|
+
amount <= held,
|
|
2444
|
+
"INVALID_AMOUNT",
|
|
2445
|
+
"Dispute-win release exceeds held paid-lot basis"
|
|
2446
|
+
);
|
|
2447
|
+
held -= amount;
|
|
2448
|
+
} else if (event.eventType === "dispute-lost") {
|
|
2449
|
+
economyAssert(
|
|
2450
|
+
amount <= held,
|
|
2451
|
+
"INVALID_AMOUNT",
|
|
2452
|
+
"Dispute-loss amount exceeds held paid-lot basis"
|
|
2453
|
+
);
|
|
2454
|
+
held -= amount;
|
|
2455
|
+
retained -= amount;
|
|
2456
|
+
reversed += amount;
|
|
2457
|
+
chargeback += amount;
|
|
2032
2458
|
} else {
|
|
2033
|
-
|
|
2459
|
+
economyAssert(
|
|
2460
|
+
amount <= retained - held,
|
|
2461
|
+
"INSUFFICIENT_ELIGIBLE_LOTS",
|
|
2462
|
+
"Paid-lot reversal exceeds unheld retained basis"
|
|
2463
|
+
);
|
|
2464
|
+
if (event.eventType === "reversal") {
|
|
2465
|
+
oneTimeReversal += amount;
|
|
2466
|
+
} else if (event.eventType === "refund") {
|
|
2467
|
+
refunded += amount;
|
|
2468
|
+
} else {
|
|
2469
|
+
chargeback += amount;
|
|
2470
|
+
}
|
|
2471
|
+
retained -= amount;
|
|
2472
|
+
reversed += amount;
|
|
2034
2473
|
}
|
|
2035
|
-
|
|
2474
|
+
effectiveEventIds.push(event.eventId);
|
|
2036
2475
|
}
|
|
2037
|
-
|
|
2476
|
+
return {
|
|
2477
|
+
retained,
|
|
2478
|
+
held,
|
|
2479
|
+
reversed,
|
|
2480
|
+
refunded,
|
|
2481
|
+
chargeback,
|
|
2482
|
+
oneTimeReversal,
|
|
2483
|
+
status: deriveStatus(retained, held, reversed, chargeback),
|
|
2484
|
+
effectiveEventIds
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
function projectionMatchesLifecycle(projection, lifecycle) {
|
|
2488
|
+
return serializeTokenSubunits(projection.retained) === lifecycle.retainedAmount && serializeTokenSubunits(projection.held) === lifecycle.heldAmount && serializeTokenSubunits(projection.reversed) === lifecycle.reversedAmount && serializeTokenSubunits(projection.refunded) === lifecycle.refundedAmount && serializeTokenSubunits(projection.chargeback) === lifecycle.chargebackAmount && serializeTokenSubunits(projection.oneTimeReversal) === lifecycle.oneTimeReversalAmount && projection.status === lifecycle.status && projection.effectiveEventIds.length === lifecycle.effectiveEventIds.length && projection.effectiveEventIds.every(
|
|
2489
|
+
(eventId, index) => eventId === lifecycle.effectiveEventIds[index]
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
function assertPaidLotLifecycle(lifecycle) {
|
|
2493
|
+
economyAssert(
|
|
2494
|
+
Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
|
|
2495
|
+
"INVALID_CONTRACT",
|
|
2496
|
+
"Paid-lot lifecycle version must match its receipt count"
|
|
2497
|
+
);
|
|
2498
|
+
const projection = projectPaidLot(
|
|
2499
|
+
immutableFactsFromLifecycle(lifecycle),
|
|
2500
|
+
lifecycle.receipts
|
|
2501
|
+
);
|
|
2502
|
+
economyAssert(
|
|
2503
|
+
projectionMatchesLifecycle(projection, lifecycle),
|
|
2504
|
+
"INVALID_CONTRACT",
|
|
2505
|
+
"Paid-lot lifecycle does not match its immutable event rebuild"
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
function createPaidLotLifecycle(lot, provenance) {
|
|
2509
|
+
assertSourceLot(lot);
|
|
2510
|
+
assertPaidLotPurchaseProvenance(provenance);
|
|
2511
|
+
economyAssert(
|
|
2512
|
+
lot.payerAccountId !== void 0 && lot.householdId !== void 0,
|
|
2513
|
+
"INVALID_CONTRACT",
|
|
2514
|
+
"Paid-lot lifecycle requires payer and receiving-household provenance"
|
|
2515
|
+
);
|
|
2516
|
+
economyAssert(
|
|
2517
|
+
lot.refundState === "none" && parseTokenSubunits(lot.heldAmount) === 0n && parseTokenSubunits(lot.reversedAmount) === 0n,
|
|
2518
|
+
"INVALID_CONTRACT",
|
|
2519
|
+
"Paid-lot lifecycle must be created before any refund or dispute movement"
|
|
2520
|
+
);
|
|
2521
|
+
economyAssert(
|
|
2522
|
+
parseIsoTimestamp(provenance.purchasedAt) <= parseIsoTimestamp(lot.settledAt),
|
|
2523
|
+
"INVALID_TIME_WINDOW",
|
|
2524
|
+
"Paid purchase time cannot follow settlement"
|
|
2525
|
+
);
|
|
2526
|
+
const facts = {
|
|
2038
2527
|
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2528
|
+
lotId: lot.lotId,
|
|
2529
|
+
payerAccountId: lot.payerAccountId,
|
|
2530
|
+
receivingHouseholdId: lot.householdId,
|
|
2531
|
+
purchaseId: provenance.purchaseId,
|
|
2532
|
+
catalogVersion: provenance.catalogVersion,
|
|
2533
|
+
purchasedAt: provenance.purchasedAt,
|
|
2534
|
+
settledAt: lot.settledAt,
|
|
2535
|
+
creditedAt: lot.creditedAt,
|
|
2536
|
+
originalAmount: lot.originalAmount
|
|
2537
|
+
};
|
|
2538
|
+
const projection = projectPaidLot(facts, []);
|
|
2539
|
+
const lifecycle = {
|
|
2540
|
+
...facts,
|
|
2541
|
+
retainedAmount: serializeTokenSubunits(projection.retained),
|
|
2542
|
+
heldAmount: serializeTokenSubunits(projection.held),
|
|
2543
|
+
reversedAmount: serializeTokenSubunits(projection.reversed),
|
|
2544
|
+
refundedAmount: serializeTokenSubunits(projection.refunded),
|
|
2545
|
+
chargebackAmount: serializeTokenSubunits(projection.chargeback),
|
|
2546
|
+
oneTimeReversalAmount: serializeTokenSubunits(
|
|
2547
|
+
projection.oneTimeReversal
|
|
2548
|
+
),
|
|
2549
|
+
status: projection.status,
|
|
2550
|
+
version: 1,
|
|
2551
|
+
receipts: [],
|
|
2552
|
+
effectiveEventIds: []
|
|
2553
|
+
};
|
|
2554
|
+
assertPaidLotLifecycle(lifecycle);
|
|
2555
|
+
return lifecycle;
|
|
2048
2556
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2557
|
+
function applyPaidLotLifecycleEvent(lifecycle, event, expectedVersion) {
|
|
2558
|
+
assertPaidLotLifecycle(lifecycle);
|
|
2559
|
+
assertPaidLotLifecycleEvent(event);
|
|
2560
|
+
economyAssert(
|
|
2561
|
+
event.lotId === lifecycle.lotId,
|
|
2562
|
+
"INVALID_CONTRACT",
|
|
2563
|
+
"Paid-lot event belongs to another source lot"
|
|
2564
|
+
);
|
|
2565
|
+
const replay = lifecycle.receipts.find(
|
|
2566
|
+
(receipt) => receipt.event.eventId === event.eventId
|
|
2567
|
+
);
|
|
2568
|
+
if (replay !== void 0) {
|
|
2569
|
+
economyAssert(
|
|
2570
|
+
sameEvent(replay.event, event),
|
|
2571
|
+
"DUPLICATE_IDENTIFIER",
|
|
2572
|
+
"Paid-lot event ID was reused with different facts"
|
|
2573
|
+
);
|
|
2574
|
+
return {
|
|
2575
|
+
lifecycle,
|
|
2576
|
+
applied: false,
|
|
2577
|
+
stateChanged: false,
|
|
2578
|
+
arithmetic: ZERO_ARITHMETIC
|
|
2579
|
+
};
|
|
2059
2580
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2581
|
+
economyAssert(
|
|
2582
|
+
expectedVersion === lifecycle.version,
|
|
2583
|
+
"INVALID_CONTRACT",
|
|
2584
|
+
"Paid-lot lifecycle event has a stale expected version"
|
|
2585
|
+
);
|
|
2586
|
+
const receipts = [
|
|
2587
|
+
...lifecycle.receipts,
|
|
2588
|
+
{ schemaVersion: ECONOMY_CONTRACT_VERSION, event }
|
|
2589
|
+
];
|
|
2590
|
+
const projection = projectPaidLot(
|
|
2591
|
+
immutableFactsFromLifecycle(lifecycle),
|
|
2592
|
+
receipts
|
|
2593
|
+
);
|
|
2594
|
+
const before = {
|
|
2595
|
+
retained: parseTokenSubunits(lifecycle.retainedAmount),
|
|
2596
|
+
held: parseTokenSubunits(lifecycle.heldAmount),
|
|
2597
|
+
reversed: parseTokenSubunits(lifecycle.reversedAmount),
|
|
2598
|
+
refunded: parseTokenSubunits(lifecycle.refundedAmount),
|
|
2599
|
+
chargeback: parseTokenSubunits(lifecycle.chargebackAmount),
|
|
2600
|
+
oneTimeReversal: parseTokenSubunits(lifecycle.oneTimeReversalAmount)
|
|
2601
|
+
};
|
|
2602
|
+
const next = {
|
|
2603
|
+
...lifecycle,
|
|
2604
|
+
retainedAmount: serializeTokenSubunits(projection.retained),
|
|
2605
|
+
heldAmount: serializeTokenSubunits(projection.held),
|
|
2606
|
+
reversedAmount: serializeTokenSubunits(projection.reversed),
|
|
2607
|
+
refundedAmount: serializeTokenSubunits(projection.refunded),
|
|
2608
|
+
chargebackAmount: serializeTokenSubunits(projection.chargeback),
|
|
2609
|
+
oneTimeReversalAmount: serializeTokenSubunits(
|
|
2610
|
+
projection.oneTimeReversal
|
|
2611
|
+
),
|
|
2612
|
+
status: projection.status,
|
|
2613
|
+
version: lifecycle.version + 1,
|
|
2614
|
+
receipts,
|
|
2615
|
+
effectiveEventIds: projection.effectiveEventIds
|
|
2616
|
+
};
|
|
2617
|
+
assertPaidLotLifecycle(next);
|
|
2618
|
+
const arithmetic = {
|
|
2619
|
+
retainedDelta: serializeTokenSubunits(
|
|
2620
|
+
projection.retained - before.retained
|
|
2621
|
+
),
|
|
2622
|
+
heldDelta: serializeTokenSubunits(projection.held - before.held),
|
|
2623
|
+
reversedDelta: serializeTokenSubunits(
|
|
2624
|
+
projection.reversed - before.reversed
|
|
2625
|
+
),
|
|
2626
|
+
refundedDelta: serializeTokenSubunits(
|
|
2627
|
+
projection.refunded - before.refunded
|
|
2628
|
+
),
|
|
2629
|
+
chargebackDelta: serializeTokenSubunits(
|
|
2630
|
+
projection.chargeback - before.chargeback
|
|
2631
|
+
),
|
|
2632
|
+
oneTimeReversalDelta: serializeTokenSubunits(
|
|
2633
|
+
projection.oneTimeReversal - before.oneTimeReversal
|
|
2634
|
+
)
|
|
2635
|
+
};
|
|
2636
|
+
return {
|
|
2637
|
+
lifecycle: next,
|
|
2638
|
+
applied: true,
|
|
2639
|
+
stateChanged: Object.values(arithmetic).some((delta) => delta !== "0"),
|
|
2640
|
+
arithmetic
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
function reducePaidLotLifecycleEvents(initial, events) {
|
|
2644
|
+
assertPaidLotLifecycle(initial);
|
|
2645
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const event of events) {
|
|
2647
|
+
assertPaidLotLifecycleEvent(event);
|
|
2648
|
+
const existing = unique.get(event.eventId);
|
|
2649
|
+
if (existing !== void 0) {
|
|
2650
|
+
economyAssert(
|
|
2651
|
+
sameEvent(existing, event),
|
|
2652
|
+
"DUPLICATE_IDENTIFIER",
|
|
2653
|
+
"Paid-lot event ID was reused with different facts"
|
|
2066
2654
|
);
|
|
2655
|
+
} else {
|
|
2656
|
+
unique.set(event.eventId, event);
|
|
2067
2657
|
}
|
|
2068
2658
|
}
|
|
2069
|
-
|
|
2070
|
-
for (const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
}
|
|
2659
|
+
let lifecycle = initial;
|
|
2660
|
+
for (const event of [...unique.values()].sort(compareEvents)) {
|
|
2661
|
+
lifecycle = applyPaidLotLifecycleEvent(
|
|
2662
|
+
lifecycle,
|
|
2663
|
+
event,
|
|
2664
|
+
lifecycle.version
|
|
2665
|
+
).lifecycle;
|
|
2102
2666
|
}
|
|
2103
|
-
return
|
|
2667
|
+
return lifecycle;
|
|
2104
2668
|
}
|
|
2105
|
-
function
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2669
|
+
function createEarlyBackerRetentionFromPaidLot(lifecycle) {
|
|
2670
|
+
assertPaidLotLifecycle(lifecycle);
|
|
2671
|
+
return {
|
|
2672
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2673
|
+
lotId: lifecycle.lotId,
|
|
2674
|
+
payerAccountId: lifecycle.payerAccountId,
|
|
2675
|
+
receivingHouseholdId: lifecycle.receivingHouseholdId,
|
|
2676
|
+
purchaseId: lifecycle.purchaseId,
|
|
2677
|
+
catalogVersion: lifecycle.catalogVersion,
|
|
2678
|
+
purchasedAt: lifecycle.purchasedAt,
|
|
2679
|
+
settledAt: lifecycle.settledAt,
|
|
2680
|
+
creditedAt: lifecycle.creditedAt,
|
|
2681
|
+
retainedAmount: lifecycle.retainedAmount
|
|
2115
2682
|
};
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// src/ports/persistence.ts
|
|
2686
|
+
function assertScopedPersistedIdempotencyResult(result) {
|
|
2687
|
+
economyAssert(
|
|
2688
|
+
result.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2689
|
+
"INVALID_CONTRACT",
|
|
2690
|
+
"Unsupported scoped idempotency-result contract version"
|
|
2691
|
+
);
|
|
2692
|
+
assertEconomyIdentifier(result.commandId, "commandId");
|
|
2693
|
+
assertEconomyIdentifier(result.transactionId, "transactionId");
|
|
2694
|
+
economyAssert(
|
|
2695
|
+
/^sha256:[a-f0-9]{64}$/u.test(result.responseHash),
|
|
2696
|
+
"INVALID_CONTRACT",
|
|
2697
|
+
"Idempotency response hash must be a canonical SHA-256 reference"
|
|
2698
|
+
);
|
|
2699
|
+
parseIsoTimestamp(result.recordedAt);
|
|
2700
|
+
}
|
|
2701
|
+
var ECONOMY_COMMAND_TYPES = /* @__PURE__ */ new Set([
|
|
2702
|
+
"credit-purchase",
|
|
2703
|
+
"credit-subscription",
|
|
2704
|
+
"credit-reward",
|
|
2705
|
+
"credit-event",
|
|
2706
|
+
"credit-competition",
|
|
2707
|
+
"allocate",
|
|
2708
|
+
"boost",
|
|
2709
|
+
"reclaim",
|
|
2710
|
+
"spend",
|
|
2711
|
+
"hold",
|
|
2712
|
+
"release-hold",
|
|
2713
|
+
"refund",
|
|
2714
|
+
"chargeback",
|
|
2715
|
+
"reverse",
|
|
2716
|
+
"adjust"
|
|
2717
|
+
]);
|
|
2718
|
+
function assertEconomyIdempotencyScope(scope) {
|
|
2719
|
+
economyAssert(
|
|
2720
|
+
scope.schemaVersion === ECONOMY_CONTRACT_VERSION && ECONOMY_COMMAND_TYPES.has(scope.commandType),
|
|
2721
|
+
"INVALID_CONTRACT",
|
|
2722
|
+
"Unsupported economy idempotency scope"
|
|
2723
|
+
);
|
|
2724
|
+
assertEconomyIdentifier(scope.idempotencyKey, "idempotencyKey");
|
|
2725
|
+
assertEconomyIdentifier(scope.actorAccountId, "actorAccountId");
|
|
2726
|
+
assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
|
|
2727
|
+
}
|
|
2728
|
+
function assertEconomyCommandEnvelope(envelope) {
|
|
2729
|
+
economyAssert(
|
|
2730
|
+
envelope.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2731
|
+
"INVALID_CONTRACT",
|
|
2732
|
+
"Unsupported economy-command-envelope contract version"
|
|
2733
|
+
);
|
|
2734
|
+
assertEconomyIdentifier(envelope.commandId, "commandId");
|
|
2735
|
+
economyAssert(
|
|
2736
|
+
ECONOMY_COMMAND_TYPES.has(envelope.commandType),
|
|
2737
|
+
"INVALID_CONTRACT",
|
|
2738
|
+
"Economy command has an unsupported type"
|
|
2739
|
+
);
|
|
2740
|
+
assertEconomyIdentifier(envelope.idempotencyKey, "idempotencyKey");
|
|
2741
|
+
assertEconomyIdentifier(envelope.actorAccountId, "actorAccountId");
|
|
2742
|
+
assertEconomyIdentifier(envelope.subjectAccountId, "subjectAccountId");
|
|
2743
|
+
if (envelope.relationshipId !== void 0) {
|
|
2744
|
+
assertEconomyIdentifier(envelope.relationshipId, "relationshipId");
|
|
2745
|
+
}
|
|
2746
|
+
if (envelope.authorizationVersion !== void 0) {
|
|
2747
|
+
economyAssert(
|
|
2748
|
+
Number.isSafeInteger(envelope.authorizationVersion) && envelope.authorizationVersion >= 1,
|
|
2126
2749
|
"INVALID_CONTRACT",
|
|
2127
|
-
"
|
|
2750
|
+
"Command authorization version must be a positive safe integer"
|
|
2128
2751
|
);
|
|
2129
|
-
|
|
2752
|
+
}
|
|
2753
|
+
economyAssert(
|
|
2754
|
+
envelope.relationshipId === void 0 === (envelope.authorizationVersion === void 0),
|
|
2755
|
+
"INVALID_CONTRACT",
|
|
2756
|
+
"Relationship identity and authorization version must be supplied together"
|
|
2757
|
+
);
|
|
2758
|
+
economyAssert(
|
|
2759
|
+
/^sha256:[a-f0-9]{64}$/u.test(envelope.payloadHash),
|
|
2760
|
+
"INVALID_CONTRACT",
|
|
2761
|
+
"Command payload hash must be a canonical SHA-256 reference"
|
|
2762
|
+
);
|
|
2763
|
+
parseIsoTimestamp(envelope.acceptedAt);
|
|
2764
|
+
assertEconomyIdentifier(envelope.acceptedRegion, "acceptedRegion");
|
|
2765
|
+
assertEconomyIdentifier(
|
|
2766
|
+
envelope.writerFencingToken,
|
|
2767
|
+
"writerFencingToken"
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
function assertEconomyCommandWorkflowEvent(event) {
|
|
2771
|
+
economyAssert(
|
|
2772
|
+
event.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2773
|
+
"INVALID_CONTRACT",
|
|
2774
|
+
"Unsupported command-workflow-event contract version"
|
|
2775
|
+
);
|
|
2776
|
+
assertEconomyIdentifier(event.workflowEventId, "workflowEventId");
|
|
2777
|
+
assertEconomyIdentifier(event.commandId, "commandId");
|
|
2778
|
+
economyAssert(
|
|
2779
|
+
["accepted", "processing", "failed", "completed"].includes(event.state),
|
|
2780
|
+
"INVALID_CONTRACT",
|
|
2781
|
+
"Command workflow has an unsupported state"
|
|
2782
|
+
);
|
|
2783
|
+
economyAssert(
|
|
2784
|
+
event.state === "failed" === (event.failureCode !== void 0),
|
|
2785
|
+
"INVALID_CONTRACT",
|
|
2786
|
+
"Only failed command workflows require a failure code"
|
|
2787
|
+
);
|
|
2788
|
+
if (event.failureCode !== void 0) {
|
|
2789
|
+
assertEconomyIdentifier(event.failureCode, "failureCode");
|
|
2790
|
+
}
|
|
2791
|
+
parseIsoTimestamp(event.occurredAt);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// src/wallets.ts
|
|
2795
|
+
function assertWalletOwnerReference(owner) {
|
|
2796
|
+
economyAssert(
|
|
2797
|
+
["account", "household", "system"].includes(owner.ownerType),
|
|
2798
|
+
"INVALID_CONTRACT",
|
|
2799
|
+
"Wallet owner reference has an unsupported type"
|
|
2800
|
+
);
|
|
2801
|
+
if (owner.ownerType === "system") {
|
|
2130
2802
|
economyAssert(
|
|
2131
|
-
|
|
2803
|
+
owner.ownerId === "system",
|
|
2804
|
+
"INVALID_CONTRACT",
|
|
2805
|
+
"System wallet lookups must use the system owner"
|
|
2806
|
+
);
|
|
2807
|
+
} else {
|
|
2808
|
+
assertEconomyIdentifier(owner.ownerId, "ownerId");
|
|
2809
|
+
economyAssert(
|
|
2810
|
+
owner.ownerId !== "system",
|
|
2811
|
+
"INVALID_CONTRACT",
|
|
2812
|
+
"Account and household wallets cannot use the system owner"
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
function assertWallet(wallet) {
|
|
2817
|
+
economyAssert(
|
|
2818
|
+
wallet.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2819
|
+
"INVALID_CONTRACT",
|
|
2820
|
+
"Unsupported wallet contract version"
|
|
2821
|
+
);
|
|
2822
|
+
assertEconomyIdentifier(wallet.walletId, "walletId");
|
|
2823
|
+
assertEconomyIdentifier(wallet.accountId, "accountId");
|
|
2824
|
+
economyAssert(
|
|
2825
|
+
[
|
|
2826
|
+
"household-treasury",
|
|
2827
|
+
"personal",
|
|
2828
|
+
"gameplay-allocation",
|
|
2829
|
+
"hold",
|
|
2830
|
+
"purchase-clearing",
|
|
2831
|
+
"provider-clearing",
|
|
2832
|
+
"system"
|
|
2833
|
+
].includes(wallet.kind),
|
|
2834
|
+
"INVALID_CONTRACT",
|
|
2835
|
+
"Wallet has an unsupported kind"
|
|
2836
|
+
);
|
|
2837
|
+
economyAssert(
|
|
2838
|
+
["account", "household", "system"].includes(wallet.ownerType),
|
|
2839
|
+
"INVALID_CONTRACT",
|
|
2840
|
+
"Wallet has an unsupported owner type"
|
|
2841
|
+
);
|
|
2842
|
+
economyAssert(
|
|
2843
|
+
["active", "restricted", "closed"].includes(wallet.status),
|
|
2844
|
+
"INVALID_CONTRACT",
|
|
2845
|
+
"Wallet has an unsupported status"
|
|
2846
|
+
);
|
|
2847
|
+
if (wallet.ownerType === "system") {
|
|
2848
|
+
economyAssert(
|
|
2849
|
+
wallet.ownerId === "system",
|
|
2850
|
+
"INVALID_CONTRACT",
|
|
2851
|
+
"System wallets must be owned by the system"
|
|
2852
|
+
);
|
|
2853
|
+
} else {
|
|
2854
|
+
assertEconomyIdentifier(wallet.ownerId, "ownerId");
|
|
2855
|
+
}
|
|
2856
|
+
if (wallet.householdId !== void 0) {
|
|
2857
|
+
assertEconomyIdentifier(wallet.householdId, "householdId");
|
|
2858
|
+
}
|
|
2859
|
+
if (wallet.ownerType === "household") {
|
|
2860
|
+
economyAssert(
|
|
2861
|
+
wallet.householdId === wallet.ownerId,
|
|
2862
|
+
"INVALID_CONTRACT",
|
|
2863
|
+
"Household wallets must identify the same household owner"
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
if (wallet.kind === "household-treasury") {
|
|
2867
|
+
economyAssert(
|
|
2868
|
+
wallet.ownerType === "household" && wallet.householdId !== void 0,
|
|
2869
|
+
"INVALID_CONTRACT",
|
|
2870
|
+
"Household treasuries must be owned by their household"
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
if (wallet.kind === "personal" || wallet.kind === "gameplay-allocation") {
|
|
2874
|
+
economyAssert(
|
|
2875
|
+
wallet.ownerType === "account",
|
|
2876
|
+
"INVALID_CONTRACT",
|
|
2877
|
+
"Personal and gameplay-allocation wallets must be account-owned"
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
if (wallet.kind === "gameplay-allocation") {
|
|
2881
|
+
economyAssert(
|
|
2882
|
+
wallet.householdId !== void 0,
|
|
2883
|
+
"INVALID_CONTRACT",
|
|
2884
|
+
"Gameplay-allocation wallets must retain household provenance"
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
if (wallet.kind === "purchase-clearing" || wallet.kind === "provider-clearing" || wallet.kind === "system") {
|
|
2888
|
+
economyAssert(
|
|
2889
|
+
wallet.ownerType === "system" && wallet.householdId === void 0,
|
|
2890
|
+
"INVALID_CONTRACT",
|
|
2891
|
+
"Clearing and system wallets must be system-owned"
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
economyAssert(
|
|
2895
|
+
Number.isSafeInteger(wallet.version) && wallet.version >= 1,
|
|
2896
|
+
"INVALID_CONTRACT",
|
|
2897
|
+
"Wallet version must be a positive safe integer"
|
|
2898
|
+
);
|
|
2899
|
+
const createdAt = parseIsoTimestamp(wallet.createdAt);
|
|
2900
|
+
if (wallet.closedAt !== void 0) {
|
|
2901
|
+
economyAssert(
|
|
2902
|
+
parseIsoTimestamp(wallet.closedAt) >= createdAt && wallet.status === "closed",
|
|
2903
|
+
"INVALID_TIME_WINDOW",
|
|
2904
|
+
"Only closed wallets may have a valid closure time"
|
|
2905
|
+
);
|
|
2906
|
+
} else {
|
|
2907
|
+
economyAssert(
|
|
2908
|
+
wallet.status !== "closed",
|
|
2909
|
+
"INVALID_CONTRACT",
|
|
2910
|
+
"Closed wallets require a closure time"
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function assertWalletBalanceSummary(summary) {
|
|
2915
|
+
economyAssert(
|
|
2916
|
+
summary.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2917
|
+
"INVALID_CONTRACT",
|
|
2918
|
+
"Unsupported wallet summary contract version"
|
|
2919
|
+
);
|
|
2920
|
+
assertEconomyIdentifier(summary.walletId, "walletId");
|
|
2921
|
+
for (const amount of [
|
|
2922
|
+
summary.available,
|
|
2923
|
+
summary.reserved,
|
|
2924
|
+
summary.held,
|
|
2925
|
+
summary.rewardProgress
|
|
2926
|
+
]) {
|
|
2927
|
+
economyAssert(
|
|
2928
|
+
parseTokenSubunits(amount) >= 0n,
|
|
2132
2929
|
"INVALID_AMOUNT",
|
|
2133
|
-
"
|
|
2930
|
+
"Wallet summary categories cannot be negative"
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
economyAssert(
|
|
2934
|
+
Number.isSafeInteger(summary.version) && summary.version >= 1,
|
|
2935
|
+
"INVALID_CONTRACT",
|
|
2936
|
+
"Wallet summary version must be a positive safe integer"
|
|
2937
|
+
);
|
|
2938
|
+
parseIsoTimestamp(summary.asOf);
|
|
2939
|
+
}
|
|
2940
|
+
function assertWalletLifetimeTotals(totals) {
|
|
2941
|
+
economyAssert(
|
|
2942
|
+
totals.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2943
|
+
"INVALID_CONTRACT",
|
|
2944
|
+
"Unsupported wallet lifetime-total contract version"
|
|
2945
|
+
);
|
|
2946
|
+
for (const amount of [
|
|
2947
|
+
totals.bought,
|
|
2948
|
+
totals.earned,
|
|
2949
|
+
totals.allocated,
|
|
2950
|
+
totals.reclaimed,
|
|
2951
|
+
totals.spent,
|
|
2952
|
+
totals.reversed
|
|
2953
|
+
]) {
|
|
2954
|
+
economyAssert(
|
|
2955
|
+
parseTokenSubunits(amount) >= 0n,
|
|
2956
|
+
"INVALID_AMOUNT",
|
|
2957
|
+
"Wallet lifetime totals cannot be negative"
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/projection.ts
|
|
2963
|
+
function assertWalletBalanceProjection(projection) {
|
|
2964
|
+
economyAssert(
|
|
2965
|
+
projection.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2966
|
+
"INVALID_CONTRACT",
|
|
2967
|
+
"Unsupported wallet-balance-projection contract version"
|
|
2968
|
+
);
|
|
2969
|
+
assertEconomyIdentifier(projection.walletId, "walletId");
|
|
2970
|
+
for (const amount of [
|
|
2971
|
+
projection.spendable,
|
|
2972
|
+
projection.reserved,
|
|
2973
|
+
projection.held
|
|
2974
|
+
]) {
|
|
2975
|
+
economyAssert(
|
|
2976
|
+
parseTokenSubunits(amount) >= 0n,
|
|
2977
|
+
"NEGATIVE_PROJECTION",
|
|
2978
|
+
"Wallet projection buckets cannot be negative"
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
economyAssert(
|
|
2982
|
+
Number.isSafeInteger(projection.version) && projection.version >= 1,
|
|
2983
|
+
"INVALID_CONTRACT",
|
|
2984
|
+
"Wallet projection version must be a positive safe integer"
|
|
2985
|
+
);
|
|
2986
|
+
if (projection.lastTransactionId !== void 0) {
|
|
2987
|
+
assertEconomyIdentifier(
|
|
2988
|
+
projection.lastTransactionId,
|
|
2989
|
+
"lastTransactionId"
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
parseIsoTimestamp(projection.asOf);
|
|
2993
|
+
}
|
|
2994
|
+
function assertWalletBalanceDelta(delta) {
|
|
2995
|
+
economyAssert(
|
|
2996
|
+
delta.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2997
|
+
"INVALID_CONTRACT",
|
|
2998
|
+
"Unsupported wallet-balance-delta contract version"
|
|
2999
|
+
);
|
|
3000
|
+
assertEconomyIdentifier(delta.walletId, "walletId");
|
|
3001
|
+
const values = [
|
|
3002
|
+
parseTokenSubunits(delta.spendableDelta),
|
|
3003
|
+
parseTokenSubunits(delta.reservedDelta),
|
|
3004
|
+
parseTokenSubunits(delta.heldDelta)
|
|
3005
|
+
];
|
|
3006
|
+
economyAssert(
|
|
3007
|
+
values.some((value) => value !== 0n),
|
|
3008
|
+
"INVALID_AMOUNT",
|
|
3009
|
+
"Wallet balance delta must change at least one bucket"
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
function assertWalletBalanceDeltaBatch(transactionId, deltas) {
|
|
3013
|
+
assertEconomyIdentifier(transactionId, "transactionId");
|
|
3014
|
+
economyAssert(
|
|
3015
|
+
deltas.length > 0,
|
|
3016
|
+
"INVALID_CONTRACT",
|
|
3017
|
+
"An economic transaction must update at least one wallet projection"
|
|
3018
|
+
);
|
|
3019
|
+
const walletIds = /* @__PURE__ */ new Set();
|
|
3020
|
+
for (const delta of deltas) {
|
|
3021
|
+
assertWalletBalanceDelta(delta);
|
|
3022
|
+
economyAssert(
|
|
3023
|
+
!walletIds.has(delta.walletId),
|
|
3024
|
+
"DUPLICATE_IDENTIFIER",
|
|
3025
|
+
"A balance-delta batch must update each wallet once"
|
|
3026
|
+
);
|
|
3027
|
+
walletIds.add(delta.walletId);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
function applyWalletBalanceDelta(projection, delta, transactionId, asOf) {
|
|
3031
|
+
assertWalletBalanceProjection(projection);
|
|
3032
|
+
assertWalletBalanceDelta(delta);
|
|
3033
|
+
assertEconomyIdentifier(transactionId, "transactionId");
|
|
3034
|
+
economyAssert(
|
|
3035
|
+
projection.walletId === delta.walletId,
|
|
3036
|
+
"INVALID_CONTRACT",
|
|
3037
|
+
"Wallet balance delta must target its projection wallet"
|
|
3038
|
+
);
|
|
3039
|
+
economyAssert(
|
|
3040
|
+
parseIsoTimestamp(asOf) >= parseIsoTimestamp(projection.asOf),
|
|
3041
|
+
"INVALID_TIME_WINDOW",
|
|
3042
|
+
"Wallet balance update cannot precede its projection"
|
|
3043
|
+
);
|
|
3044
|
+
const next = {
|
|
3045
|
+
...projection,
|
|
3046
|
+
spendable: serializeTokenSubunits(
|
|
3047
|
+
parseTokenSubunits(projection.spendable) + parseTokenSubunits(delta.spendableDelta)
|
|
3048
|
+
),
|
|
3049
|
+
reserved: serializeTokenSubunits(
|
|
3050
|
+
parseTokenSubunits(projection.reserved) + parseTokenSubunits(delta.reservedDelta)
|
|
3051
|
+
),
|
|
3052
|
+
held: serializeTokenSubunits(
|
|
3053
|
+
parseTokenSubunits(projection.held) + parseTokenSubunits(delta.heldDelta)
|
|
3054
|
+
),
|
|
3055
|
+
version: projection.version + 1,
|
|
3056
|
+
lastTransactionId: transactionId,
|
|
3057
|
+
asOf
|
|
3058
|
+
};
|
|
3059
|
+
assertWalletBalanceProjection(next);
|
|
3060
|
+
return next;
|
|
3061
|
+
}
|
|
3062
|
+
function createWalletBalanceSummary(projection) {
|
|
3063
|
+
assertWalletBalanceProjection(projection);
|
|
3064
|
+
const spendable = parseTokenSubunits(projection.spendable);
|
|
3065
|
+
const rewardProgress = spendable % 1000n;
|
|
3066
|
+
const summary = {
|
|
3067
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3068
|
+
walletId: projection.walletId,
|
|
3069
|
+
available: serializeTokenSubunits(spendable - rewardProgress),
|
|
3070
|
+
reserved: projection.reserved,
|
|
3071
|
+
held: projection.held,
|
|
3072
|
+
rewardProgress: serializeTokenSubunits(rewardProgress),
|
|
3073
|
+
version: projection.version,
|
|
3074
|
+
asOf: projection.asOf
|
|
3075
|
+
};
|
|
3076
|
+
assertDeterministicWalletBalanceSummary(summary);
|
|
3077
|
+
return summary;
|
|
3078
|
+
}
|
|
3079
|
+
function assertDeterministicWalletBalanceSummary(summary) {
|
|
3080
|
+
assertWalletBalanceSummary(summary);
|
|
3081
|
+
economyAssert(
|
|
3082
|
+
parseTokenSubunits(summary.available) % 1000n === 0n && parseTokenSubunits(summary.rewardProgress) < 1000n,
|
|
3083
|
+
"INVALID_CONTRACT",
|
|
3084
|
+
"Per-wallet availability must be whole Tokens with sub-Token progress below one Token"
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
function deriveWalletBalanceDeltas(transaction, bindings) {
|
|
3088
|
+
assertEconomicJournalTransaction(transaction);
|
|
3089
|
+
const byAccount = /* @__PURE__ */ new Map();
|
|
3090
|
+
for (const binding of bindings) {
|
|
3091
|
+
assertEconomyIdentifier(binding.accountId, "accountId");
|
|
3092
|
+
assertEconomyIdentifier(binding.walletId, "walletId");
|
|
3093
|
+
economyAssert(
|
|
3094
|
+
["spendable", "reserved", "held"].includes(binding.bucket),
|
|
3095
|
+
"INVALID_CONTRACT",
|
|
3096
|
+
"Wallet account binding has an unsupported bucket"
|
|
3097
|
+
);
|
|
3098
|
+
economyAssert(
|
|
3099
|
+
!byAccount.has(binding.accountId),
|
|
3100
|
+
"DUPLICATE_IDENTIFIER",
|
|
3101
|
+
"A ledger account may bind to only one wallet bucket"
|
|
3102
|
+
);
|
|
3103
|
+
byAccount.set(binding.accountId, binding);
|
|
3104
|
+
}
|
|
3105
|
+
const byWallet = /* @__PURE__ */ new Map();
|
|
3106
|
+
for (const posting of transaction.postings) {
|
|
3107
|
+
if (posting.walletId === void 0) {
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
const binding = byAccount.get(posting.accountId);
|
|
3111
|
+
economyAssert(
|
|
3112
|
+
binding !== void 0 && binding.walletId === posting.walletId,
|
|
3113
|
+
"INVALID_CONTRACT",
|
|
3114
|
+
"Every wallet-linked posting must match an authoritative account binding"
|
|
3115
|
+
);
|
|
3116
|
+
const values = byWallet.get(binding.walletId) ?? [0n, 0n, 0n];
|
|
3117
|
+
const amount = parseTokenSubunits(posting.amount);
|
|
3118
|
+
if (binding.bucket === "spendable") {
|
|
3119
|
+
values[0] += amount;
|
|
3120
|
+
} else if (binding.bucket === "reserved") {
|
|
3121
|
+
values[1] += amount;
|
|
3122
|
+
} else {
|
|
3123
|
+
values[2] += amount;
|
|
3124
|
+
}
|
|
3125
|
+
byWallet.set(binding.walletId, values);
|
|
3126
|
+
}
|
|
3127
|
+
const deltas = [...byWallet.entries()].map(([walletId, values]) => ({
|
|
3128
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3129
|
+
walletId,
|
|
3130
|
+
spendableDelta: serializeTokenSubunits(values[0]),
|
|
3131
|
+
reservedDelta: serializeTokenSubunits(values[1]),
|
|
3132
|
+
heldDelta: serializeTokenSubunits(values[2])
|
|
3133
|
+
})).filter(
|
|
3134
|
+
(delta) => delta.spendableDelta !== "0" || delta.reservedDelta !== "0" || delta.heldDelta !== "0"
|
|
3135
|
+
).sort((left, right) => left.walletId.localeCompare(right.walletId));
|
|
3136
|
+
assertWalletBalanceDeltaBatch(transaction.transactionId, deltas);
|
|
3137
|
+
return deltas;
|
|
3138
|
+
}
|
|
3139
|
+
var EARNED_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
|
|
3140
|
+
"rewarded-ad",
|
|
3141
|
+
"offerwall",
|
|
3142
|
+
"event",
|
|
3143
|
+
"competition"
|
|
3144
|
+
]);
|
|
3145
|
+
function deriveWalletLifetimeDeltas(transaction) {
|
|
3146
|
+
assertEconomicJournalTransaction(transaction);
|
|
3147
|
+
if (transaction.status !== "settled") {
|
|
3148
|
+
return [];
|
|
3149
|
+
}
|
|
3150
|
+
const netByWallet = /* @__PURE__ */ new Map();
|
|
3151
|
+
for (const posting of transaction.postings) {
|
|
3152
|
+
if (posting.walletId !== void 0) {
|
|
3153
|
+
netByWallet.set(
|
|
3154
|
+
posting.walletId,
|
|
3155
|
+
(netByWallet.get(posting.walletId) ?? 0n) + parseTokenSubunits(posting.amount)
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const deltas = [];
|
|
3160
|
+
for (const [walletId, net] of [...netByWallet.entries()].sort(
|
|
3161
|
+
([left], [right]) => left.localeCompare(right)
|
|
3162
|
+
)) {
|
|
3163
|
+
let bucket;
|
|
3164
|
+
let amount = 0n;
|
|
3165
|
+
if (net > 0n && (transaction.activityType === "purchase" || transaction.activityType === "subscription")) {
|
|
3166
|
+
bucket = "bought";
|
|
3167
|
+
amount = net;
|
|
3168
|
+
} else if (net > 0n && EARNED_ACTIVITY_TYPES.has(transaction.activityType)) {
|
|
3169
|
+
bucket = "earned";
|
|
3170
|
+
amount = net;
|
|
3171
|
+
} else if (net < 0n && (transaction.activityType === "allocation" || transaction.activityType === "boost")) {
|
|
3172
|
+
bucket = "allocated";
|
|
3173
|
+
amount = -net;
|
|
3174
|
+
} else if (net > 0n && transaction.activityType === "reclaim") {
|
|
3175
|
+
bucket = "reclaimed";
|
|
3176
|
+
amount = net;
|
|
3177
|
+
} else if (net < 0n && transaction.activityType === "spend") {
|
|
3178
|
+
bucket = "spent";
|
|
3179
|
+
amount = -net;
|
|
3180
|
+
} else if (net < 0n && (transaction.activityType === "refund" || transaction.activityType === "chargeback" || transaction.activityType === "reversal")) {
|
|
3181
|
+
bucket = "reversed";
|
|
3182
|
+
amount = -net;
|
|
3183
|
+
}
|
|
3184
|
+
if (bucket !== void 0) {
|
|
3185
|
+
deltas.push({
|
|
3186
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3187
|
+
walletId,
|
|
3188
|
+
bucket,
|
|
3189
|
+
amount: serializeTokenSubunits(amount)
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
return deltas;
|
|
3194
|
+
}
|
|
3195
|
+
function applyWalletLifetimeDeltas(current, walletId, deltas) {
|
|
3196
|
+
assertWalletLifetimeTotals(current);
|
|
3197
|
+
assertEconomyIdentifier(walletId, "walletId");
|
|
3198
|
+
const next = {
|
|
3199
|
+
bought: parseTokenSubunits(current.bought),
|
|
3200
|
+
earned: parseTokenSubunits(current.earned),
|
|
3201
|
+
allocated: parseTokenSubunits(current.allocated),
|
|
3202
|
+
reclaimed: parseTokenSubunits(current.reclaimed),
|
|
3203
|
+
spent: parseTokenSubunits(current.spent),
|
|
3204
|
+
reversed: parseTokenSubunits(current.reversed)
|
|
3205
|
+
};
|
|
3206
|
+
for (const delta of deltas) {
|
|
3207
|
+
economyAssert(
|
|
3208
|
+
delta.schemaVersion === ECONOMY_CONTRACT_VERSION && delta.walletId === walletId && [
|
|
3209
|
+
"bought",
|
|
3210
|
+
"earned",
|
|
3211
|
+
"allocated",
|
|
3212
|
+
"reclaimed",
|
|
3213
|
+
"spent",
|
|
3214
|
+
"reversed"
|
|
3215
|
+
].includes(delta.bucket),
|
|
3216
|
+
"INVALID_CONTRACT",
|
|
3217
|
+
"Lifetime delta must target the requested wallet and a known bucket"
|
|
3218
|
+
);
|
|
3219
|
+
const amount = parseTokenSubunits(delta.amount);
|
|
3220
|
+
economyAssert(
|
|
3221
|
+
amount > 0n,
|
|
3222
|
+
"INVALID_AMOUNT",
|
|
3223
|
+
"Lifetime deltas must be positive"
|
|
3224
|
+
);
|
|
3225
|
+
next[delta.bucket] += amount;
|
|
3226
|
+
}
|
|
3227
|
+
const totals = {
|
|
3228
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3229
|
+
bought: serializeTokenSubunits(next.bought),
|
|
3230
|
+
earned: serializeTokenSubunits(next.earned),
|
|
3231
|
+
allocated: serializeTokenSubunits(next.allocated),
|
|
3232
|
+
reclaimed: serializeTokenSubunits(next.reclaimed),
|
|
3233
|
+
spent: serializeTokenSubunits(next.spent),
|
|
3234
|
+
reversed: serializeTokenSubunits(next.reversed)
|
|
3235
|
+
};
|
|
3236
|
+
assertWalletLifetimeTotals(totals);
|
|
3237
|
+
return totals;
|
|
3238
|
+
}
|
|
3239
|
+
function applyTransactionToProjection(current, transaction) {
|
|
3240
|
+
assertBalancedTransaction(transaction);
|
|
3241
|
+
const next = { ...current };
|
|
3242
|
+
for (const posting of transaction.postings) {
|
|
3243
|
+
const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
|
|
3244
|
+
next[posting.accountId] = serializeTokenSubunits(
|
|
3245
|
+
prior + parseTokenSubunits(posting.amount)
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
return next;
|
|
3249
|
+
}
|
|
3250
|
+
function rebuildBalanceProjection(transactions) {
|
|
3251
|
+
let projection = {};
|
|
3252
|
+
const transactionIds = /* @__PURE__ */ new Set();
|
|
3253
|
+
const idempotencyKeys = /* @__PURE__ */ new Set();
|
|
3254
|
+
const providerEvents = /* @__PURE__ */ new Set();
|
|
3255
|
+
for (const transaction of transactions) {
|
|
3256
|
+
economyAssert(
|
|
3257
|
+
!transactionIds.has(transaction.transactionId),
|
|
3258
|
+
"DUPLICATE_TRANSACTION",
|
|
3259
|
+
"Transaction identifiers must be unique"
|
|
3260
|
+
);
|
|
3261
|
+
economyAssert(
|
|
3262
|
+
!idempotencyKeys.has(transaction.idempotencyKey),
|
|
3263
|
+
"DUPLICATE_TRANSACTION",
|
|
3264
|
+
"Idempotency keys must be unique"
|
|
3265
|
+
);
|
|
3266
|
+
if (transaction.providerEventId !== void 0) {
|
|
3267
|
+
economyAssert(
|
|
3268
|
+
!providerEvents.has(transaction.providerEventId),
|
|
3269
|
+
"DUPLICATE_TRANSACTION",
|
|
3270
|
+
"Provider event identifiers must be unique"
|
|
3271
|
+
);
|
|
3272
|
+
providerEvents.add(transaction.providerEventId);
|
|
3273
|
+
}
|
|
3274
|
+
transactionIds.add(transaction.transactionId);
|
|
3275
|
+
idempotencyKeys.add(transaction.idempotencyKey);
|
|
3276
|
+
projection = applyTransactionToProjection(projection, transaction);
|
|
3277
|
+
}
|
|
3278
|
+
return projection;
|
|
3279
|
+
}
|
|
3280
|
+
function assertNonNegativeAccounts(projection, protectedAccountIds) {
|
|
3281
|
+
for (const accountId of protectedAccountIds) {
|
|
3282
|
+
economyAssert(
|
|
3283
|
+
parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
|
|
3284
|
+
"NEGATIVE_PROJECTION",
|
|
3285
|
+
"A protected economy account would become negative"
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function createProjectionSnapshot(transactions, rebuiltAt) {
|
|
3290
|
+
parseIsoTimestamp(rebuiltAt);
|
|
3291
|
+
const last = transactions.at(-1);
|
|
3292
|
+
return {
|
|
3293
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3294
|
+
balances: rebuildBalanceProjection(transactions),
|
|
3295
|
+
...last === void 0 ? {} : { lastTransactionId: last.transactionId },
|
|
3296
|
+
rebuiltAt
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// src/ports/query.ts
|
|
3301
|
+
var ACTIVITY_TYPES2 = /* @__PURE__ */ new Set([
|
|
3302
|
+
"purchase",
|
|
3303
|
+
"subscription",
|
|
3304
|
+
"rewarded-ad",
|
|
3305
|
+
"offerwall",
|
|
3306
|
+
"allocation",
|
|
3307
|
+
"boost",
|
|
3308
|
+
"reclaim",
|
|
3309
|
+
"spend",
|
|
3310
|
+
"hold",
|
|
3311
|
+
"refund",
|
|
3312
|
+
"chargeback",
|
|
3313
|
+
"adjustment",
|
|
3314
|
+
"reversal",
|
|
3315
|
+
"event",
|
|
3316
|
+
"competition"
|
|
3317
|
+
]);
|
|
3318
|
+
var ACTIVITY_STATUSES2 = /* @__PURE__ */ new Set([
|
|
3319
|
+
"pending",
|
|
3320
|
+
"held",
|
|
3321
|
+
"settled",
|
|
3322
|
+
"reversed",
|
|
3323
|
+
"failed"
|
|
3324
|
+
]);
|
|
3325
|
+
var ACTIVITY_SOURCES2 = /* @__PURE__ */ new Set([
|
|
3326
|
+
"shopify",
|
|
3327
|
+
"ayet",
|
|
3328
|
+
"bitlabs",
|
|
3329
|
+
"subscription",
|
|
3330
|
+
"event",
|
|
3331
|
+
"competition",
|
|
3332
|
+
"adjustment"
|
|
3333
|
+
]);
|
|
3334
|
+
var PORTFOLIO_ROLES = /* @__PURE__ */ new Set([
|
|
3335
|
+
"household-treasury",
|
|
3336
|
+
"personal",
|
|
3337
|
+
"gameplay-allocation",
|
|
3338
|
+
"hold"
|
|
3339
|
+
]);
|
|
3340
|
+
function hasControlCharacters2(value) {
|
|
3341
|
+
return [...value].some((character) => {
|
|
3342
|
+
const code = character.charCodeAt(0);
|
|
3343
|
+
return code <= 31 || code === 127;
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
function assertSafeDisplayString(value, label) {
|
|
3347
|
+
economyAssert(
|
|
3348
|
+
typeof value === "string" && value.trim().length > 0 && value.length <= 128 && !hasControlCharacters2(value),
|
|
3349
|
+
"INVALID_CONTRACT",
|
|
3350
|
+
`${label} must be bounded and safe for display`
|
|
3351
|
+
);
|
|
3352
|
+
}
|
|
3353
|
+
function assertOpaqueCursor(value) {
|
|
3354
|
+
economyAssert(
|
|
3355
|
+
typeof value === "string" && value.length > 0 && value.length <= 512 && !hasControlCharacters2(value),
|
|
3356
|
+
"INVALID_CONTRACT",
|
|
3357
|
+
"Activity cursor must be a bounded opaque value"
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
function assertUniqueValues(values, allowed, label) {
|
|
3361
|
+
const unique = new Set(values);
|
|
3362
|
+
economyAssert(
|
|
3363
|
+
values.length > 0 && values.length === unique.size && values.every((value) => allowed.has(value)),
|
|
3364
|
+
"INVALID_CONTRACT",
|
|
3365
|
+
`${label} must contain unique supported values`
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
function assertWalletPortfolioReadScope(scope) {
|
|
3369
|
+
economyAssert(
|
|
3370
|
+
scope.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3371
|
+
"INVALID_CONTRACT",
|
|
3372
|
+
"Unsupported wallet-portfolio scope contract version"
|
|
3373
|
+
);
|
|
3374
|
+
assertEconomyIdentifier(scope.portfolioId, "portfolioId");
|
|
3375
|
+
assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
|
|
3376
|
+
economyAssert(
|
|
3377
|
+
scope.components.length > 0 && scope.components.length <= 100,
|
|
3378
|
+
"INVALID_CONTRACT",
|
|
3379
|
+
"Wallet portfolio must contain between one and 100 components"
|
|
3380
|
+
);
|
|
3381
|
+
const walletIds = /* @__PURE__ */ new Set();
|
|
3382
|
+
for (const component of scope.components) {
|
|
3383
|
+
assertEconomyIdentifier(component.walletId, "walletId");
|
|
3384
|
+
economyAssert(
|
|
3385
|
+
PORTFOLIO_ROLES.has(component.role),
|
|
3386
|
+
"INVALID_CONTRACT",
|
|
3387
|
+
"Wallet portfolio component has an unsupported role"
|
|
3388
|
+
);
|
|
3389
|
+
economyAssert(
|
|
3390
|
+
!walletIds.has(component.walletId),
|
|
3391
|
+
"DUPLICATE_IDENTIFIER",
|
|
3392
|
+
"Wallet portfolio cannot repeat a wallet"
|
|
3393
|
+
);
|
|
3394
|
+
walletIds.add(component.walletId);
|
|
3395
|
+
if (component.beneficiaryAccountId !== void 0) {
|
|
3396
|
+
assertEconomyIdentifier(
|
|
3397
|
+
component.beneficiaryAccountId,
|
|
3398
|
+
"beneficiaryAccountId"
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
economyAssert(
|
|
3402
|
+
component.role !== "gameplay-allocation" || component.beneficiaryAccountId !== void 0,
|
|
3403
|
+
"INVALID_CONTRACT",
|
|
3404
|
+
"Gameplay-allocation portfolio components require a beneficiary"
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
function assertWalletLifetimeSnapshot(snapshot) {
|
|
3409
|
+
economyAssert(
|
|
3410
|
+
snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3411
|
+
"INVALID_CONTRACT",
|
|
3412
|
+
"Unsupported wallet-lifetime-snapshot contract version"
|
|
3413
|
+
);
|
|
3414
|
+
assertEconomyIdentifier(snapshot.walletId, "walletId");
|
|
3415
|
+
assertWalletLifetimeTotals(snapshot.totals);
|
|
3416
|
+
economyAssert(
|
|
3417
|
+
Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
|
|
3418
|
+
"INVALID_CONTRACT",
|
|
3419
|
+
"Wallet lifetime version must be a positive safe integer"
|
|
3420
|
+
);
|
|
3421
|
+
parseIsoTimestamp(snapshot.asOf);
|
|
3422
|
+
}
|
|
3423
|
+
function assertPortfolioComponentMatch(expected, actual) {
|
|
3424
|
+
economyAssert(
|
|
3425
|
+
expected.walletId === actual.walletId && expected.role === actual.role && expected.beneficiaryAccountId === actual.beneficiaryAccountId,
|
|
3426
|
+
"INVALID_CONTRACT",
|
|
3427
|
+
"Portfolio result component does not match its authorized read scope"
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
function createWalletPortfolioSummary(scope, components, asOf) {
|
|
3431
|
+
assertWalletPortfolioReadScope(scope);
|
|
3432
|
+
parseIsoTimestamp(asOf);
|
|
3433
|
+
economyAssert(
|
|
3434
|
+
components.length === scope.components.length,
|
|
3435
|
+
"INVALID_CONTRACT",
|
|
3436
|
+
"Portfolio balance must include every authorized component exactly once"
|
|
3437
|
+
);
|
|
3438
|
+
const byWallet = new Map(components.map((item) => [item.walletId, item]));
|
|
3439
|
+
economyAssert(
|
|
3440
|
+
byWallet.size === components.length,
|
|
3441
|
+
"DUPLICATE_IDENTIFIER",
|
|
3442
|
+
"Portfolio balance cannot repeat a wallet"
|
|
3443
|
+
);
|
|
3444
|
+
const ordered = scope.components.map((expected) => {
|
|
3445
|
+
const actual = byWallet.get(expected.walletId);
|
|
3446
|
+
economyAssert(
|
|
3447
|
+
actual !== void 0,
|
|
3448
|
+
"INVALID_CONTRACT",
|
|
3449
|
+
"Portfolio balance is missing an authorized wallet"
|
|
3450
|
+
);
|
|
3451
|
+
assertPortfolioComponentMatch(expected, actual);
|
|
3452
|
+
assertDeterministicWalletBalanceSummary(actual.summary);
|
|
3453
|
+
economyAssert(
|
|
3454
|
+
actual.summary.walletId === actual.walletId && actual.summary.asOf === asOf,
|
|
3455
|
+
"INVALID_CONTRACT",
|
|
3456
|
+
"Portfolio component summary must identify its wallet and shared snapshot time"
|
|
3457
|
+
);
|
|
3458
|
+
return actual;
|
|
3459
|
+
});
|
|
3460
|
+
const totals = {
|
|
3461
|
+
available: serializeTokenSubunits(
|
|
3462
|
+
ordered.reduce(
|
|
3463
|
+
(sum, item) => sum + parseTokenSubunits(item.summary.available),
|
|
3464
|
+
0n
|
|
3465
|
+
)
|
|
3466
|
+
),
|
|
3467
|
+
reserved: serializeTokenSubunits(
|
|
3468
|
+
ordered.reduce(
|
|
3469
|
+
(sum, item) => sum + parseTokenSubunits(item.summary.reserved),
|
|
3470
|
+
0n
|
|
3471
|
+
)
|
|
3472
|
+
),
|
|
3473
|
+
held: serializeTokenSubunits(
|
|
3474
|
+
ordered.reduce(
|
|
3475
|
+
(sum, item) => sum + parseTokenSubunits(item.summary.held),
|
|
3476
|
+
0n
|
|
3477
|
+
)
|
|
3478
|
+
),
|
|
3479
|
+
rewardProgress: serializeTokenSubunits(
|
|
3480
|
+
ordered.reduce(
|
|
3481
|
+
(sum, item) => sum + parseTokenSubunits(item.summary.rewardProgress),
|
|
3482
|
+
0n
|
|
3483
|
+
)
|
|
3484
|
+
)
|
|
3485
|
+
};
|
|
3486
|
+
return {
|
|
3487
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3488
|
+
portfolioId: scope.portfolioId,
|
|
3489
|
+
subjectAccountId: scope.subjectAccountId,
|
|
3490
|
+
components: ordered,
|
|
3491
|
+
totals,
|
|
3492
|
+
asOf
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
function assertWalletPortfolioSummary(summary) {
|
|
3496
|
+
const scope = {
|
|
3497
|
+
schemaVersion: summary.schemaVersion,
|
|
3498
|
+
portfolioId: summary.portfolioId,
|
|
3499
|
+
subjectAccountId: summary.subjectAccountId,
|
|
3500
|
+
components: summary.components.map(
|
|
3501
|
+
({ walletId, role, beneficiaryAccountId }) => ({
|
|
3502
|
+
walletId,
|
|
3503
|
+
role,
|
|
3504
|
+
...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
|
|
3505
|
+
})
|
|
3506
|
+
)
|
|
3507
|
+
};
|
|
3508
|
+
const rebuilt = createWalletPortfolioSummary(
|
|
3509
|
+
scope,
|
|
3510
|
+
summary.components,
|
|
3511
|
+
summary.asOf
|
|
3512
|
+
);
|
|
3513
|
+
for (const field of [
|
|
3514
|
+
"available",
|
|
3515
|
+
"reserved",
|
|
3516
|
+
"held",
|
|
3517
|
+
"rewardProgress"
|
|
3518
|
+
]) {
|
|
3519
|
+
economyAssert(
|
|
3520
|
+
summary.totals[field] === rebuilt.totals[field],
|
|
3521
|
+
"INVALID_CONTRACT",
|
|
3522
|
+
"Portfolio balance totals must equal their component column sums"
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function createWalletPortfolioLifetime(scope, components, asOf) {
|
|
3527
|
+
assertWalletPortfolioReadScope(scope);
|
|
3528
|
+
parseIsoTimestamp(asOf);
|
|
3529
|
+
economyAssert(
|
|
3530
|
+
components.length === scope.components.length,
|
|
3531
|
+
"INVALID_CONTRACT",
|
|
3532
|
+
"Portfolio lifetime must include every authorized component exactly once"
|
|
3533
|
+
);
|
|
3534
|
+
const byWallet = new Map(components.map((item) => [item.walletId, item]));
|
|
3535
|
+
economyAssert(
|
|
3536
|
+
byWallet.size === components.length,
|
|
3537
|
+
"DUPLICATE_IDENTIFIER",
|
|
3538
|
+
"Portfolio lifetime cannot repeat a wallet"
|
|
3539
|
+
);
|
|
3540
|
+
const ordered = scope.components.map((expected) => {
|
|
3541
|
+
const actual = byWallet.get(expected.walletId);
|
|
3542
|
+
economyAssert(
|
|
3543
|
+
actual !== void 0,
|
|
3544
|
+
"INVALID_CONTRACT",
|
|
3545
|
+
"Portfolio lifetime is missing an authorized wallet"
|
|
3546
|
+
);
|
|
3547
|
+
assertPortfolioComponentMatch(expected, actual);
|
|
3548
|
+
assertWalletLifetimeSnapshot(actual.snapshot);
|
|
3549
|
+
economyAssert(
|
|
3550
|
+
actual.snapshot.walletId === actual.walletId && actual.snapshot.asOf === asOf,
|
|
3551
|
+
"INVALID_CONTRACT",
|
|
3552
|
+
"Portfolio lifetime component must identify its wallet and shared snapshot time"
|
|
2134
3553
|
);
|
|
2135
|
-
|
|
2136
|
-
}
|
|
3554
|
+
return actual;
|
|
3555
|
+
});
|
|
3556
|
+
const sumField = (field) => serializeTokenSubunits(
|
|
3557
|
+
ordered.reduce(
|
|
3558
|
+
(sum, item) => sum + parseTokenSubunits(item.snapshot.totals[field]),
|
|
3559
|
+
0n
|
|
3560
|
+
)
|
|
3561
|
+
);
|
|
2137
3562
|
const totals = {
|
|
2138
3563
|
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2139
|
-
bought:
|
|
2140
|
-
earned:
|
|
2141
|
-
allocated:
|
|
2142
|
-
reclaimed:
|
|
2143
|
-
spent:
|
|
2144
|
-
reversed:
|
|
3564
|
+
bought: sumField("bought"),
|
|
3565
|
+
earned: sumField("earned"),
|
|
3566
|
+
allocated: sumField("allocated"),
|
|
3567
|
+
reclaimed: sumField("reclaimed"),
|
|
3568
|
+
spent: sumField("spent"),
|
|
3569
|
+
reversed: sumField("reversed")
|
|
2145
3570
|
};
|
|
2146
3571
|
assertWalletLifetimeTotals(totals);
|
|
2147
|
-
return
|
|
3572
|
+
return {
|
|
3573
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3574
|
+
portfolioId: scope.portfolioId,
|
|
3575
|
+
subjectAccountId: scope.subjectAccountId,
|
|
3576
|
+
components: ordered,
|
|
3577
|
+
totals,
|
|
3578
|
+
asOf
|
|
3579
|
+
};
|
|
2148
3580
|
}
|
|
2149
|
-
function
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
3581
|
+
function assertWalletPortfolioLifetime(lifetime) {
|
|
3582
|
+
const scope = {
|
|
3583
|
+
schemaVersion: lifetime.schemaVersion,
|
|
3584
|
+
portfolioId: lifetime.portfolioId,
|
|
3585
|
+
subjectAccountId: lifetime.subjectAccountId,
|
|
3586
|
+
components: lifetime.components.map(
|
|
3587
|
+
({ walletId, role, beneficiaryAccountId }) => ({
|
|
3588
|
+
walletId,
|
|
3589
|
+
role,
|
|
3590
|
+
...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
|
|
3591
|
+
})
|
|
3592
|
+
)
|
|
3593
|
+
};
|
|
3594
|
+
const rebuilt = createWalletPortfolioLifetime(
|
|
3595
|
+
scope,
|
|
3596
|
+
lifetime.components,
|
|
3597
|
+
lifetime.asOf
|
|
3598
|
+
);
|
|
3599
|
+
for (const field of [
|
|
3600
|
+
"bought",
|
|
3601
|
+
"earned",
|
|
3602
|
+
"allocated",
|
|
3603
|
+
"reclaimed",
|
|
3604
|
+
"spent",
|
|
3605
|
+
"reversed"
|
|
3606
|
+
]) {
|
|
3607
|
+
economyAssert(
|
|
3608
|
+
lifetime.totals[field] === rebuilt.totals[field],
|
|
3609
|
+
"INVALID_CONTRACT",
|
|
3610
|
+
"Portfolio lifetime totals must equal their component column sums"
|
|
2156
3611
|
);
|
|
2157
3612
|
}
|
|
2158
|
-
return next;
|
|
2159
3613
|
}
|
|
2160
|
-
function
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
3614
|
+
function assertWalletActivityEntry(entry) {
|
|
3615
|
+
economyAssert(
|
|
3616
|
+
entry.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3617
|
+
"INVALID_CONTRACT",
|
|
3618
|
+
"Unsupported wallet-activity contract version"
|
|
3619
|
+
);
|
|
3620
|
+
assertEconomyIdentifier(entry.activityId, "activityId");
|
|
3621
|
+
assertEconomyIdentifier(entry.walletId, "walletId");
|
|
3622
|
+
economyAssert(
|
|
3623
|
+
ACTIVITY_TYPES2.has(entry.activityType) && ACTIVITY_STATUSES2.has(entry.status) && ACTIVITY_SOURCES2.has(entry.source),
|
|
3624
|
+
"INVALID_CONTRACT",
|
|
3625
|
+
"Wallet activity type, status, or source is unsupported"
|
|
3626
|
+
);
|
|
3627
|
+
parseIsoTimestamp(entry.occurredAt);
|
|
3628
|
+
economyAssert(
|
|
3629
|
+
parseTokenSubunits(entry.amount) !== 0n,
|
|
3630
|
+
"INVALID_AMOUNT",
|
|
3631
|
+
"Wallet activity amount cannot be zero"
|
|
3632
|
+
);
|
|
3633
|
+
if (entry.beneficiaryAccountId !== void 0) {
|
|
3634
|
+
assertEconomyIdentifier(
|
|
3635
|
+
entry.beneficiaryAccountId,
|
|
3636
|
+
"beneficiaryAccountId"
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
if (entry.maskedReference !== void 0) {
|
|
3640
|
+
assertSafeDisplayString(entry.maskedReference, "Masked reference");
|
|
3641
|
+
}
|
|
3642
|
+
assertSafeDisplayString(entry.sourceLabel, "Source label");
|
|
3643
|
+
economyAssert(
|
|
3644
|
+
entry.entryKind === "economic" || entry.entryKind === "workflow",
|
|
3645
|
+
"INVALID_CONTRACT",
|
|
3646
|
+
"Wallet activity has an unsupported entry kind"
|
|
3647
|
+
);
|
|
3648
|
+
if (entry.entryKind === "economic") {
|
|
3649
|
+
assertEconomyIdentifier(entry.transactionId, "transactionId");
|
|
3650
|
+
economyAssert(
|
|
3651
|
+
entry.status === "held" || entry.status === "settled" || entry.status === "reversed",
|
|
3652
|
+
"INVALID_CONTRACT",
|
|
3653
|
+
"Economic activity cannot use a workflow status"
|
|
3654
|
+
);
|
|
3655
|
+
} else {
|
|
3656
|
+
assertEconomyIdentifier(entry.commandId, "commandId");
|
|
3657
|
+
economyAssert(
|
|
3658
|
+
entry.status === "pending" || entry.status === "failed",
|
|
3659
|
+
"INVALID_CONTRACT",
|
|
3660
|
+
"Workflow activity cannot use an economic status"
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
function assertWalletActivityPageRequest(request) {
|
|
3665
|
+
economyAssert(
|
|
3666
|
+
Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 100,
|
|
3667
|
+
"INVALID_CONTRACT",
|
|
3668
|
+
"Activity page limit must be between one and 100"
|
|
3669
|
+
);
|
|
3670
|
+
if (request.cursor !== void 0) {
|
|
3671
|
+
assertOpaqueCursor(request.cursor);
|
|
3672
|
+
}
|
|
3673
|
+
const filter = request.filter;
|
|
3674
|
+
if (filter === void 0) {
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
if (filter.activityTypes !== void 0) {
|
|
3678
|
+
assertUniqueValues(filter.activityTypes, ACTIVITY_TYPES2, "Activity types");
|
|
3679
|
+
}
|
|
3680
|
+
if (filter.statuses !== void 0) {
|
|
3681
|
+
assertUniqueValues(filter.statuses, ACTIVITY_STATUSES2, "Activity statuses");
|
|
3682
|
+
}
|
|
3683
|
+
if (filter.sources !== void 0) {
|
|
3684
|
+
assertUniqueValues(filter.sources, ACTIVITY_SOURCES2, "Activity sources");
|
|
3685
|
+
}
|
|
3686
|
+
if (filter.beneficiaryAccountId !== void 0) {
|
|
3687
|
+
assertEconomyIdentifier(
|
|
3688
|
+
filter.beneficiaryAccountId,
|
|
3689
|
+
"beneficiaryAccountId"
|
|
3690
|
+
);
|
|
3691
|
+
}
|
|
3692
|
+
const from = filter.fromInclusive === void 0 ? void 0 : parseIsoTimestamp(filter.fromInclusive);
|
|
3693
|
+
const to = filter.toExclusive === void 0 ? void 0 : parseIsoTimestamp(filter.toExclusive);
|
|
3694
|
+
economyAssert(
|
|
3695
|
+
from === void 0 || to === void 0 || from < to,
|
|
3696
|
+
"INVALID_TIME_WINDOW",
|
|
3697
|
+
"Activity filter start must precede its exclusive end"
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
function assertWalletActivityPage(page) {
|
|
3701
|
+
economyAssert(
|
|
3702
|
+
page.schemaVersion === ECONOMY_CONTRACT_VERSION && typeof page.hasMore === "boolean",
|
|
3703
|
+
"INVALID_CONTRACT",
|
|
3704
|
+
"Unsupported wallet-activity-page contract"
|
|
3705
|
+
);
|
|
3706
|
+
economyAssert(
|
|
3707
|
+
page.hasMore === (page.nextCursor !== void 0),
|
|
3708
|
+
"INVALID_CONTRACT",
|
|
3709
|
+
"Activity page cursor must be present exactly when more rows exist"
|
|
3710
|
+
);
|
|
3711
|
+
economyAssert(
|
|
3712
|
+
page.entries.length <= 100 && (!page.hasMore || page.entries.length > 0),
|
|
3713
|
+
"INVALID_CONTRACT",
|
|
3714
|
+
"Activity pages are bounded and cannot advertise more rows from an empty page"
|
|
3715
|
+
);
|
|
3716
|
+
if (page.nextCursor !== void 0) {
|
|
3717
|
+
assertOpaqueCursor(page.nextCursor);
|
|
3718
|
+
}
|
|
3719
|
+
const activityIds = /* @__PURE__ */ new Set();
|
|
3720
|
+
let previous;
|
|
3721
|
+
for (const entry of page.entries) {
|
|
3722
|
+
assertWalletActivityEntry(entry);
|
|
3723
|
+
economyAssert(
|
|
3724
|
+
!activityIds.has(entry.activityId),
|
|
3725
|
+
"DUPLICATE_IDENTIFIER",
|
|
3726
|
+
"Activity page cannot repeat an activity ID"
|
|
3727
|
+
);
|
|
3728
|
+
activityIds.add(entry.activityId);
|
|
3729
|
+
if (previous !== void 0) {
|
|
3730
|
+
const previousTime = parseIsoTimestamp(previous.occurredAt);
|
|
3731
|
+
const currentTime = parseIsoTimestamp(entry.occurredAt);
|
|
3732
|
+
economyAssert(
|
|
3733
|
+
previousTime > currentTime || previousTime === currentTime && previous.activityId.localeCompare(entry.activityId) > 0,
|
|
3734
|
+
"INVALID_CONTRACT",
|
|
3735
|
+
"Activity page must use stable descending time and ID order"
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
previous = entry;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
function assertWalletActivityPageForPortfolio(page, scope) {
|
|
3742
|
+
assertWalletActivityPage(page);
|
|
3743
|
+
assertWalletPortfolioReadScope(scope);
|
|
3744
|
+
const walletIds = new Set(scope.components.map((item) => item.walletId));
|
|
3745
|
+
economyAssert(
|
|
3746
|
+
page.entries.every((entry) => walletIds.has(entry.walletId)),
|
|
3747
|
+
"INVALID_CONTRACT",
|
|
3748
|
+
"Portfolio activity contains a wallet outside its authorized read scope"
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
// src/purchase-caps.ts
|
|
3753
|
+
var MAX_SIGNED_BIGINT = 2n ** 63n - 1n;
|
|
3754
|
+
var CANONICAL_NON_NEGATIVE_INTEGER = /^(?:0|[1-9][0-9]*)$/u;
|
|
3755
|
+
function parseGbpMinorUnits(value) {
|
|
3756
|
+
economyAssert(
|
|
3757
|
+
typeof value === "string" && CANONICAL_NON_NEGATIVE_INTEGER.test(value),
|
|
3758
|
+
"INVALID_AMOUNT",
|
|
3759
|
+
"GBP minor units must be a canonical non-negative integer string"
|
|
3760
|
+
);
|
|
3761
|
+
try {
|
|
3762
|
+
const amount = BigInt(value);
|
|
3763
|
+
economyAssert(
|
|
3764
|
+
amount <= MAX_SIGNED_BIGINT,
|
|
3765
|
+
"AMOUNT_OUT_OF_RANGE",
|
|
3766
|
+
"GBP minor units are outside the signed 64-bit range"
|
|
3767
|
+
);
|
|
3768
|
+
return amount;
|
|
3769
|
+
} catch (error) {
|
|
3770
|
+
if (error instanceof EconomyError) {
|
|
3771
|
+
throw error;
|
|
3772
|
+
}
|
|
3773
|
+
throw new EconomyError("INVALID_AMOUNT", "GBP minor units are invalid");
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
function serializeGbpMinorUnits(amount) {
|
|
3777
|
+
economyAssert(
|
|
3778
|
+
amount >= 0n && amount <= MAX_SIGNED_BIGINT,
|
|
3779
|
+
"AMOUNT_OUT_OF_RANGE",
|
|
3780
|
+
"GBP minor units are outside the signed 64-bit range"
|
|
3781
|
+
);
|
|
3782
|
+
return amount.toString(10);
|
|
3783
|
+
}
|
|
3784
|
+
var RESERVATION_STATUSES = [
|
|
3785
|
+
"reserved",
|
|
3786
|
+
"settled",
|
|
3787
|
+
"released",
|
|
3788
|
+
"expired"
|
|
3789
|
+
];
|
|
3790
|
+
var TRANSITION_PRECEDENCE = {
|
|
3791
|
+
settle: 0,
|
|
3792
|
+
release: 1,
|
|
3793
|
+
expire: 2
|
|
3794
|
+
};
|
|
3795
|
+
function assertPurchaseCapReservationTransition(transition) {
|
|
3796
|
+
economyAssert(
|
|
3797
|
+
transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3798
|
+
"INVALID_CONTRACT",
|
|
3799
|
+
"Unsupported purchase-cap transition contract version"
|
|
3800
|
+
);
|
|
3801
|
+
assertEconomyIdentifier(transition.transitionId, "transitionId");
|
|
3802
|
+
assertEconomyIdentifier(transition.reservationId, "reservationId");
|
|
3803
|
+
economyAssert(
|
|
3804
|
+
["settle", "release", "expire"].includes(transition.transitionType),
|
|
3805
|
+
"INVALID_CONTRACT",
|
|
3806
|
+
"Purchase-cap transition type is unsupported"
|
|
3807
|
+
);
|
|
3808
|
+
parseIsoTimestamp(transition.occurredAt);
|
|
3809
|
+
}
|
|
3810
|
+
function sameTransition(left, right) {
|
|
3811
|
+
return left.schemaVersion === right.schemaVersion && left.transitionId === right.transitionId && left.reservationId === right.reservationId && left.transitionType === right.transitionType && left.occurredAt === right.occurredAt;
|
|
3812
|
+
}
|
|
3813
|
+
function compareTransitions(left, right) {
|
|
3814
|
+
const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
|
|
3815
|
+
if (timeDifference !== 0) {
|
|
3816
|
+
return timeDifference;
|
|
3817
|
+
}
|
|
3818
|
+
const precedenceDifference = TRANSITION_PRECEDENCE[left.transitionType] - TRANSITION_PRECEDENCE[right.transitionType];
|
|
3819
|
+
return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
|
|
3820
|
+
}
|
|
3821
|
+
function assertBinding(binding) {
|
|
3822
|
+
economyAssert(
|
|
3823
|
+
binding.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3824
|
+
"INVALID_CONTRACT",
|
|
3825
|
+
"Unsupported purchase-cap reservation contract version"
|
|
3826
|
+
);
|
|
3827
|
+
assertEconomyIdentifier(binding.reservationId, "reservationId");
|
|
3828
|
+
assertEconomyIdentifier(binding.payerAccountId, "payerAccountId");
|
|
3829
|
+
assertEconomyIdentifier(binding.householdId, "householdId");
|
|
3830
|
+
economyAssert(
|
|
3831
|
+
parseGbpMinorUnits(binding.priceMinorUnits) > 0n,
|
|
3832
|
+
"INVALID_AMOUNT",
|
|
3833
|
+
"Purchase-cap reservation amount must be positive"
|
|
3834
|
+
);
|
|
3835
|
+
economyAssert(
|
|
3836
|
+
parseIsoTimestamp(binding.expiresAt) > parseIsoTimestamp(binding.reservedAt),
|
|
3837
|
+
"INVALID_TIME_WINDOW",
|
|
3838
|
+
"Purchase-cap reservation expiry must follow its creation"
|
|
3839
|
+
);
|
|
3840
|
+
}
|
|
3841
|
+
function bindingFromReservation(reservation) {
|
|
3842
|
+
return {
|
|
3843
|
+
schemaVersion: reservation.schemaVersion,
|
|
3844
|
+
reservationId: reservation.reservationId,
|
|
3845
|
+
payerAccountId: reservation.payerAccountId,
|
|
3846
|
+
householdId: reservation.householdId,
|
|
3847
|
+
priceMinorUnits: reservation.priceMinorUnits,
|
|
3848
|
+
reservedAt: reservation.reservedAt,
|
|
3849
|
+
expiresAt: reservation.expiresAt
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
function projectReservation(binding, receipts) {
|
|
3853
|
+
assertBinding(binding);
|
|
3854
|
+
const reservedAt = parseIsoTimestamp(binding.reservedAt);
|
|
3855
|
+
const expiresAt = parseIsoTimestamp(binding.expiresAt);
|
|
3856
|
+
const transitionIds = /* @__PURE__ */ new Set();
|
|
3857
|
+
for (const receipt of receipts) {
|
|
2166
3858
|
economyAssert(
|
|
2167
|
-
|
|
2168
|
-
"
|
|
2169
|
-
"
|
|
3859
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3860
|
+
"INVALID_CONTRACT",
|
|
3861
|
+
"Unsupported purchase-cap transition receipt version"
|
|
2170
3862
|
);
|
|
3863
|
+
assertPurchaseCapReservationTransition(receipt.transition);
|
|
2171
3864
|
economyAssert(
|
|
2172
|
-
!
|
|
2173
|
-
"
|
|
2174
|
-
"
|
|
3865
|
+
receipt.transition.reservationId === binding.reservationId && !transitionIds.has(receipt.transition.transitionId),
|
|
3866
|
+
"DUPLICATE_IDENTIFIER",
|
|
3867
|
+
"Purchase-cap receipts must be unique and belong to the reservation"
|
|
2175
3868
|
);
|
|
2176
|
-
|
|
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);
|
|
3869
|
+
transitionIds.add(receipt.transition.transitionId);
|
|
2187
3870
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
for (const
|
|
3871
|
+
let status = "reserved";
|
|
3872
|
+
let finalTransitionId;
|
|
3873
|
+
let finalizedAt;
|
|
3874
|
+
for (const receipt of [...receipts].sort(
|
|
3875
|
+
(left, right) => compareTransitions(left.transition, right.transition)
|
|
3876
|
+
)) {
|
|
3877
|
+
const transition = receipt.transition;
|
|
3878
|
+
const occurredAt = parseIsoTimestamp(transition.occurredAt);
|
|
2192
3879
|
economyAssert(
|
|
2193
|
-
|
|
2194
|
-
"
|
|
2195
|
-
"
|
|
3880
|
+
occurredAt >= reservedAt,
|
|
3881
|
+
"INVALID_TIME_WINDOW",
|
|
3882
|
+
"Purchase-cap transition cannot precede reservation"
|
|
2196
3883
|
);
|
|
3884
|
+
if (transition.transitionType === "settle") {
|
|
3885
|
+
economyAssert(
|
|
3886
|
+
occurredAt < expiresAt,
|
|
3887
|
+
"INVALID_TIME_WINDOW",
|
|
3888
|
+
"Purchase-cap settlement must use payment time before expiry"
|
|
3889
|
+
);
|
|
3890
|
+
} else if (transition.transitionType === "release") {
|
|
3891
|
+
economyAssert(
|
|
3892
|
+
occurredAt < expiresAt,
|
|
3893
|
+
"INVALID_TIME_WINDOW",
|
|
3894
|
+
"Purchase-cap release must precede reservation expiry"
|
|
3895
|
+
);
|
|
3896
|
+
} else {
|
|
3897
|
+
economyAssert(
|
|
3898
|
+
occurredAt >= expiresAt,
|
|
3899
|
+
"INVALID_TIME_WINDOW",
|
|
3900
|
+
"Purchase-cap reservation cannot expire before its deadline"
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3903
|
+
if (status === "reserved") {
|
|
3904
|
+
status = transition.transitionType === "settle" ? "settled" : transition.transitionType === "release" ? "released" : "expired";
|
|
3905
|
+
finalTransitionId = transition.transitionId;
|
|
3906
|
+
finalizedAt = transition.occurredAt;
|
|
3907
|
+
}
|
|
2197
3908
|
}
|
|
2198
|
-
}
|
|
2199
|
-
function createProjectionSnapshot(transactions, rebuiltAt) {
|
|
2200
|
-
parseIsoTimestamp(rebuiltAt);
|
|
2201
|
-
const last = transactions.at(-1);
|
|
2202
3909
|
return {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
...
|
|
2206
|
-
|
|
3910
|
+
...binding,
|
|
3911
|
+
status,
|
|
3912
|
+
...finalTransitionId === void 0 ? {} : { finalTransitionId, finalizedAt },
|
|
3913
|
+
transitionReceipts: receipts
|
|
2207
3914
|
};
|
|
2208
3915
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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;
|
|
3916
|
+
function sameReservationFacts(left, right) {
|
|
3917
|
+
return left.schemaVersion === right.schemaVersion && left.reservationId === right.reservationId && left.payerAccountId === right.payerAccountId && left.householdId === right.householdId && left.priceMinorUnits === right.priceMinorUnits && left.status === right.status && left.reservedAt === right.reservedAt && left.expiresAt === right.expiresAt && left.finalTransitionId === right.finalTransitionId && left.finalizedAt === right.finalizedAt && left.transitionReceipts.length === right.transitionReceipts.length && left.transitionReceipts.every((receipt, index) => {
|
|
3918
|
+
const other = right.transitionReceipts[index];
|
|
3919
|
+
return other !== void 0 && receipt.schemaVersion === other.schemaVersion && sameTransition(receipt.transition, other.transition);
|
|
2254
3920
|
});
|
|
2255
3921
|
}
|
|
2256
|
-
function
|
|
2257
|
-
|
|
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
|
-
);
|
|
3922
|
+
function sameReservationBinding(reservation, proposed) {
|
|
3923
|
+
return reservation.schemaVersion === proposed.schemaVersion && reservation.reservationId === proposed.reservationId && reservation.payerAccountId === proposed.payerAccountId && reservation.householdId === proposed.householdId && reservation.priceMinorUnits === proposed.priceMinorUnits && reservation.reservedAt === proposed.reservedAt && reservation.expiresAt === proposed.expiresAt;
|
|
2262
3924
|
}
|
|
2263
|
-
function
|
|
3925
|
+
function assertPurchaseCapReservation(reservation) {
|
|
2264
3926
|
economyAssert(
|
|
2265
|
-
|
|
3927
|
+
RESERVATION_STATUSES.includes(reservation.status),
|
|
2266
3928
|
"INVALID_CONTRACT",
|
|
2267
|
-
"
|
|
3929
|
+
"Purchase-cap reservation status is unsupported"
|
|
3930
|
+
);
|
|
3931
|
+
const projected = projectReservation(
|
|
3932
|
+
bindingFromReservation(reservation),
|
|
3933
|
+
reservation.transitionReceipts
|
|
2268
3934
|
);
|
|
2269
|
-
}
|
|
2270
|
-
function assertUniqueValues(values, allowed, label) {
|
|
2271
|
-
const unique = new Set(values);
|
|
2272
3935
|
economyAssert(
|
|
2273
|
-
|
|
3936
|
+
sameReservationFacts(projected, reservation),
|
|
2274
3937
|
"INVALID_CONTRACT",
|
|
2275
|
-
|
|
3938
|
+
"Purchase-cap reservation does not match its immutable event rebuild"
|
|
2276
3939
|
);
|
|
2277
3940
|
}
|
|
2278
|
-
function
|
|
3941
|
+
function assertRollingPurchaseCapState(state) {
|
|
2279
3942
|
economyAssert(
|
|
2280
|
-
|
|
3943
|
+
state.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2281
3944
|
"INVALID_CONTRACT",
|
|
2282
|
-
"Unsupported
|
|
3945
|
+
"Unsupported rolling purchase-cap contract version"
|
|
2283
3946
|
);
|
|
2284
|
-
assertEconomyIdentifier(scope.portfolioId, "portfolioId");
|
|
2285
|
-
assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
|
|
2286
3947
|
economyAssert(
|
|
2287
|
-
|
|
3948
|
+
state.scopeType === "payer" || state.scopeType === "household",
|
|
2288
3949
|
"INVALID_CONTRACT",
|
|
2289
|
-
"
|
|
2290
|
-
);
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
3950
|
+
"Purchase-cap scope type is unsupported"
|
|
3951
|
+
);
|
|
3952
|
+
assertEconomyIdentifier(state.scopeId, "scopeId");
|
|
3953
|
+
const reservationIds = /* @__PURE__ */ new Set();
|
|
3954
|
+
const transitionIds = /* @__PURE__ */ new Set();
|
|
3955
|
+
let receiptCount = 0;
|
|
3956
|
+
for (const reservation of state.reservations) {
|
|
3957
|
+
assertPurchaseCapReservation(reservation);
|
|
2294
3958
|
economyAssert(
|
|
2295
|
-
|
|
2296
|
-
"INVALID_CONTRACT",
|
|
2297
|
-
"Wallet portfolio component has an unsupported role"
|
|
2298
|
-
);
|
|
2299
|
-
economyAssert(
|
|
2300
|
-
!walletIds.has(component.walletId),
|
|
3959
|
+
!reservationIds.has(reservation.reservationId),
|
|
2301
3960
|
"DUPLICATE_IDENTIFIER",
|
|
2302
|
-
"
|
|
3961
|
+
"Purchase-cap state cannot repeat a reservation"
|
|
2303
3962
|
);
|
|
2304
|
-
|
|
2305
|
-
if (component.beneficiaryAccountId !== void 0) {
|
|
2306
|
-
assertEconomyIdentifier(
|
|
2307
|
-
component.beneficiaryAccountId,
|
|
2308
|
-
"beneficiaryAccountId"
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
3963
|
+
reservationIds.add(reservation.reservationId);
|
|
2311
3964
|
economyAssert(
|
|
2312
|
-
|
|
3965
|
+
state.scopeType === "payer" ? reservation.payerAccountId === state.scopeId : reservation.householdId === state.scopeId,
|
|
2313
3966
|
"INVALID_CONTRACT",
|
|
2314
|
-
"
|
|
3967
|
+
"Purchase-cap reservation belongs to another aggregate scope"
|
|
2315
3968
|
);
|
|
3969
|
+
for (const receipt of reservation.transitionReceipts) {
|
|
3970
|
+
economyAssert(
|
|
3971
|
+
!transitionIds.has(receipt.transition.transitionId),
|
|
3972
|
+
"DUPLICATE_IDENTIFIER",
|
|
3973
|
+
"Purchase-cap state cannot repeat a transition ID"
|
|
3974
|
+
);
|
|
3975
|
+
transitionIds.add(receipt.transition.transitionId);
|
|
3976
|
+
receiptCount += 1;
|
|
3977
|
+
}
|
|
2316
3978
|
}
|
|
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
3979
|
economyAssert(
|
|
2327
|
-
Number.isSafeInteger(
|
|
3980
|
+
Number.isSafeInteger(state.version) && state.version === 1 + state.reservations.length + receiptCount,
|
|
2328
3981
|
"INVALID_CONTRACT",
|
|
2329
|
-
"
|
|
3982
|
+
"Purchase-cap state version must match reservation and receipt counts"
|
|
2330
3983
|
);
|
|
2331
|
-
parseIsoTimestamp(snapshot.asOf);
|
|
2332
3984
|
}
|
|
2333
|
-
function
|
|
3985
|
+
function assertAggregatePair(payerState, householdState, payerAccountId, householdId) {
|
|
3986
|
+
assertRollingPurchaseCapState(payerState);
|
|
3987
|
+
assertRollingPurchaseCapState(householdState);
|
|
2334
3988
|
economyAssert(
|
|
2335
|
-
|
|
3989
|
+
payerState.scopeType === "payer" && payerState.scopeId === payerAccountId && householdState.scopeType === "household" && householdState.scopeId === householdId,
|
|
2336
3990
|
"INVALID_CONTRACT",
|
|
2337
|
-
"
|
|
3991
|
+
"Purchase-cap aggregate pair does not match payer and household"
|
|
2338
3992
|
);
|
|
2339
3993
|
}
|
|
2340
|
-
function
|
|
2341
|
-
|
|
2342
|
-
|
|
3994
|
+
function findMirroredReservation(payerState, householdState, reservationId) {
|
|
3995
|
+
const payerReservation = payerState.reservations.find(
|
|
3996
|
+
(reservation) => reservation.reservationId === reservationId
|
|
3997
|
+
);
|
|
3998
|
+
const householdReservation = householdState.reservations.find(
|
|
3999
|
+
(reservation) => reservation.reservationId === reservationId
|
|
4000
|
+
);
|
|
2343
4001
|
economyAssert(
|
|
2344
|
-
|
|
4002
|
+
payerReservation === void 0 === (householdReservation === void 0),
|
|
2345
4003
|
"INVALID_CONTRACT",
|
|
2346
|
-
"
|
|
4004
|
+
"Purchase-cap reservation must exist in both aggregate scopes atomically"
|
|
2347
4005
|
);
|
|
2348
|
-
|
|
4006
|
+
if (payerReservation === void 0 || householdReservation === void 0) {
|
|
4007
|
+
return void 0;
|
|
4008
|
+
}
|
|
2349
4009
|
economyAssert(
|
|
2350
|
-
|
|
2351
|
-
"
|
|
2352
|
-
"
|
|
4010
|
+
sameReservationFacts(payerReservation, householdReservation),
|
|
4011
|
+
"INVALID_CONTRACT",
|
|
4012
|
+
"Payer and household purchase-cap reservations have diverged"
|
|
2353
4013
|
);
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
4014
|
+
return payerReservation;
|
|
4015
|
+
}
|
|
4016
|
+
function windowStart(checkedAt, rollingDays) {
|
|
4017
|
+
return checkedAt - rollingDays * 24 * 60 * 60 * 1e3;
|
|
4018
|
+
}
|
|
4019
|
+
function calculateRollingPurchaseCapUsage(state, checkedAt, policy) {
|
|
4020
|
+
assertRollingPurchaseCapState(state);
|
|
4021
|
+
assertPurchaseLimitPolicy(policy);
|
|
4022
|
+
const checkedAtValue = parseIsoTimestamp(checkedAt);
|
|
4023
|
+
const start = windowStart(checkedAtValue, policy.rollingDays);
|
|
4024
|
+
let total = 0n;
|
|
4025
|
+
for (const reservation of state.reservations) {
|
|
4026
|
+
if (reservation.status === "reserved" && parseIsoTimestamp(reservation.reservedAt) <= checkedAtValue && checkedAtValue < parseIsoTimestamp(reservation.expiresAt)) {
|
|
4027
|
+
total += parseGbpMinorUnits(reservation.priceMinorUnits);
|
|
4028
|
+
} else if (reservation.status === "settled" && reservation.finalizedAt !== void 0) {
|
|
4029
|
+
const settledAt = parseIsoTimestamp(reservation.finalizedAt);
|
|
4030
|
+
if (settledAt >= start && settledAt <= checkedAtValue) {
|
|
4031
|
+
total += parseGbpMinorUnits(reservation.priceMinorUnits);
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
2363
4034
|
economyAssert(
|
|
2364
|
-
|
|
2365
|
-
"
|
|
2366
|
-
"
|
|
4035
|
+
total <= MAX_SIGNED_BIGINT,
|
|
4036
|
+
"AMOUNT_OUT_OF_RANGE",
|
|
4037
|
+
"Rolling purchase-cap usage exceeds signed 64-bit range"
|
|
2367
4038
|
);
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
};
|
|
4039
|
+
}
|
|
4040
|
+
return serializeGbpMinorUnits(total);
|
|
4041
|
+
}
|
|
4042
|
+
function createRollingPurchaseCapState(scopeType, scopeId) {
|
|
4043
|
+
economyAssert(
|
|
4044
|
+
scopeType === "payer" || scopeType === "household",
|
|
4045
|
+
"INVALID_CONTRACT",
|
|
4046
|
+
"Purchase-cap scope type is unsupported"
|
|
4047
|
+
);
|
|
4048
|
+
assertEconomyIdentifier(scopeId, "scopeId");
|
|
2396
4049
|
return {
|
|
2397
4050
|
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
asOf
|
|
4051
|
+
scopeType,
|
|
4052
|
+
scopeId,
|
|
4053
|
+
version: 1,
|
|
4054
|
+
reservations: []
|
|
2403
4055
|
};
|
|
2404
4056
|
}
|
|
2405
|
-
function
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
4057
|
+
function sortedWithReservation(state, reservation) {
|
|
4058
|
+
return [...state.reservations, reservation].sort(
|
|
4059
|
+
(left, right) => compareUnicodeCodeUnits(left.reservationId, right.reservationId)
|
|
4060
|
+
);
|
|
4061
|
+
}
|
|
4062
|
+
function resultWithUsage(payerState, householdState, reservation, applied, stateChanged, checkedAt, policy) {
|
|
4063
|
+
return {
|
|
4064
|
+
payerState,
|
|
4065
|
+
householdState,
|
|
4066
|
+
reservation,
|
|
4067
|
+
applied,
|
|
4068
|
+
stateChanged,
|
|
4069
|
+
payerUsageMinorUnits: calculateRollingPurchaseCapUsage(
|
|
4070
|
+
payerState,
|
|
4071
|
+
checkedAt,
|
|
4072
|
+
policy
|
|
4073
|
+
),
|
|
4074
|
+
householdUsageMinorUnits: calculateRollingPurchaseCapUsage(
|
|
4075
|
+
householdState,
|
|
4076
|
+
checkedAt,
|
|
4077
|
+
policy
|
|
2416
4078
|
)
|
|
2417
4079
|
};
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
4080
|
+
}
|
|
4081
|
+
function reserveRollingPurchaseCaps(payerState, householdState, command, policy) {
|
|
4082
|
+
economyAssert(
|
|
4083
|
+
command.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
4084
|
+
"INVALID_CONTRACT",
|
|
4085
|
+
"Unsupported purchase-cap reservation command version"
|
|
2422
4086
|
);
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
4087
|
+
const binding = {
|
|
4088
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
4089
|
+
reservationId: command.reservationId,
|
|
4090
|
+
payerAccountId: command.payerAccountId,
|
|
4091
|
+
householdId: command.householdId,
|
|
4092
|
+
priceMinorUnits: command.priceMinorUnits,
|
|
4093
|
+
reservedAt: command.reservedAt,
|
|
4094
|
+
expiresAt: command.expiresAt
|
|
4095
|
+
};
|
|
4096
|
+
assertBinding(binding);
|
|
4097
|
+
assertAggregatePair(
|
|
4098
|
+
payerState,
|
|
4099
|
+
householdState,
|
|
4100
|
+
command.payerAccountId,
|
|
4101
|
+
command.householdId
|
|
4102
|
+
);
|
|
4103
|
+
assertPurchaseLimitPolicy(policy);
|
|
4104
|
+
const proposed = projectReservation(binding, []);
|
|
4105
|
+
const replay = findMirroredReservation(
|
|
4106
|
+
payerState,
|
|
4107
|
+
householdState,
|
|
4108
|
+
command.reservationId
|
|
4109
|
+
);
|
|
4110
|
+
if (replay !== void 0) {
|
|
2429
4111
|
economyAssert(
|
|
2430
|
-
|
|
2431
|
-
"
|
|
2432
|
-
"
|
|
4112
|
+
sameReservationBinding(replay, proposed),
|
|
4113
|
+
"DUPLICATE_IDENTIFIER",
|
|
4114
|
+
"Purchase-cap reservation ID was reused with different facts"
|
|
4115
|
+
);
|
|
4116
|
+
return resultWithUsage(
|
|
4117
|
+
payerState,
|
|
4118
|
+
householdState,
|
|
4119
|
+
replay,
|
|
4120
|
+
false,
|
|
4121
|
+
false,
|
|
4122
|
+
replay.finalizedAt ?? command.reservedAt,
|
|
4123
|
+
policy
|
|
2433
4124
|
);
|
|
2434
4125
|
}
|
|
2435
|
-
}
|
|
2436
|
-
function createWalletPortfolioLifetime(scope, components, asOf) {
|
|
2437
|
-
assertWalletPortfolioReadScope(scope);
|
|
2438
|
-
parseIsoTimestamp(asOf);
|
|
2439
4126
|
economyAssert(
|
|
2440
|
-
|
|
4127
|
+
command.expectedPayerVersion === payerState.version && command.expectedHouseholdVersion === householdState.version,
|
|
2441
4128
|
"INVALID_CONTRACT",
|
|
2442
|
-
"
|
|
4129
|
+
"Purchase-cap reservation has a stale aggregate version"
|
|
2443
4130
|
);
|
|
2444
|
-
const
|
|
4131
|
+
const amount = parseGbpMinorUnits(command.priceMinorUnits);
|
|
2445
4132
|
economyAssert(
|
|
2446
|
-
|
|
2447
|
-
"
|
|
2448
|
-
"
|
|
4133
|
+
amount <= parseGbpMinorUnits(policy.maxOrderPriceMinorUnits),
|
|
4134
|
+
"INSUFFICIENT_BALANCE",
|
|
4135
|
+
"Purchase amount exceeds the per-order ceiling"
|
|
2449
4136
|
);
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
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
|
|
4137
|
+
const payerUsage = parseGbpMinorUnits(
|
|
4138
|
+
calculateRollingPurchaseCapUsage(payerState, command.reservedAt, policy)
|
|
4139
|
+
);
|
|
4140
|
+
const householdUsage = parseGbpMinorUnits(
|
|
4141
|
+
calculateRollingPurchaseCapUsage(
|
|
4142
|
+
householdState,
|
|
4143
|
+
command.reservedAt,
|
|
4144
|
+
policy
|
|
2470
4145
|
)
|
|
2471
4146
|
);
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
4147
|
+
economyAssert(
|
|
4148
|
+
payerUsage + amount <= parseGbpMinorUnits(policy.rollingPayerPriceMinorUnits),
|
|
4149
|
+
"INSUFFICIENT_BALANCE",
|
|
4150
|
+
"Purchase amount exceeds the rolling payer ceiling"
|
|
4151
|
+
);
|
|
4152
|
+
economyAssert(
|
|
4153
|
+
householdUsage + amount <= parseGbpMinorUnits(policy.rollingHouseholdPriceMinorUnits),
|
|
4154
|
+
"INSUFFICIENT_BALANCE",
|
|
4155
|
+
"Purchase amount exceeds the rolling household ceiling"
|
|
4156
|
+
);
|
|
4157
|
+
const nextPayer = {
|
|
4158
|
+
...payerState,
|
|
4159
|
+
version: payerState.version + 1,
|
|
4160
|
+
reservations: sortedWithReservation(payerState, proposed)
|
|
2480
4161
|
};
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
subjectAccountId: scope.subjectAccountId,
|
|
2486
|
-
components: ordered,
|
|
2487
|
-
totals,
|
|
2488
|
-
asOf
|
|
4162
|
+
const nextHousehold = {
|
|
4163
|
+
...householdState,
|
|
4164
|
+
version: householdState.version + 1,
|
|
4165
|
+
reservations: sortedWithReservation(householdState, proposed)
|
|
2489
4166
|
};
|
|
4167
|
+
return resultWithUsage(
|
|
4168
|
+
nextPayer,
|
|
4169
|
+
nextHousehold,
|
|
4170
|
+
proposed,
|
|
4171
|
+
true,
|
|
4172
|
+
true,
|
|
4173
|
+
command.reservedAt,
|
|
4174
|
+
policy
|
|
4175
|
+
);
|
|
2490
4176
|
}
|
|
2491
|
-
function
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
({ walletId, role, beneficiaryAccountId }) => ({
|
|
2498
|
-
walletId,
|
|
2499
|
-
role,
|
|
2500
|
-
...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
|
|
2501
|
-
})
|
|
4177
|
+
function replaceReservation(state, replacement) {
|
|
4178
|
+
return {
|
|
4179
|
+
...state,
|
|
4180
|
+
version: state.version + 1,
|
|
4181
|
+
reservations: state.reservations.map(
|
|
4182
|
+
(reservation) => reservation.reservationId === replacement.reservationId ? replacement : reservation
|
|
2502
4183
|
)
|
|
2503
4184
|
};
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
4185
|
+
}
|
|
4186
|
+
function findTransitionById(state, transitionId) {
|
|
4187
|
+
for (const reservation of state.reservations) {
|
|
4188
|
+
const receipt = reservation.transitionReceipts.find(
|
|
4189
|
+
(candidate) => candidate.transition.transitionId === transitionId
|
|
4190
|
+
);
|
|
4191
|
+
if (receipt !== void 0) {
|
|
4192
|
+
return { reservationId: reservation.reservationId, transition: receipt.transition };
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
return void 0;
|
|
4196
|
+
}
|
|
4197
|
+
function transitionRollingPurchaseCapReservation(payerState, householdState, transition, expectedPayerVersion, expectedHouseholdVersion, policy) {
|
|
4198
|
+
assertPurchaseCapReservationTransition(transition);
|
|
4199
|
+
assertRollingPurchaseCapState(payerState);
|
|
4200
|
+
assertRollingPurchaseCapState(householdState);
|
|
4201
|
+
const current = findMirroredReservation(
|
|
4202
|
+
payerState,
|
|
4203
|
+
householdState,
|
|
4204
|
+
transition.reservationId
|
|
2508
4205
|
);
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
"
|
|
2512
|
-
"
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
4206
|
+
economyAssert(
|
|
4207
|
+
current !== void 0,
|
|
4208
|
+
"INVALID_CONTRACT",
|
|
4209
|
+
"Purchase-cap reservation does not exist in both aggregate scopes"
|
|
4210
|
+
);
|
|
4211
|
+
assertAggregatePair(
|
|
4212
|
+
payerState,
|
|
4213
|
+
householdState,
|
|
4214
|
+
current.payerAccountId,
|
|
4215
|
+
current.householdId
|
|
4216
|
+
);
|
|
4217
|
+
assertPurchaseLimitPolicy(policy);
|
|
4218
|
+
const payerExisting = findTransitionById(
|
|
4219
|
+
payerState,
|
|
4220
|
+
transition.transitionId
|
|
4221
|
+
);
|
|
4222
|
+
const householdExisting = findTransitionById(
|
|
4223
|
+
householdState,
|
|
4224
|
+
transition.transitionId
|
|
4225
|
+
);
|
|
4226
|
+
economyAssert(
|
|
4227
|
+
payerExisting === void 0 === (householdExisting === void 0),
|
|
4228
|
+
"INVALID_CONTRACT",
|
|
4229
|
+
"Purchase-cap transition must exist in both scopes atomically"
|
|
4230
|
+
);
|
|
4231
|
+
if (payerExisting !== void 0 && householdExisting !== void 0) {
|
|
2517
4232
|
economyAssert(
|
|
2518
|
-
|
|
2519
|
-
"
|
|
2520
|
-
"
|
|
4233
|
+
payerExisting.reservationId === transition.reservationId && householdExisting.reservationId === transition.reservationId && sameTransition(payerExisting.transition, transition) && sameTransition(householdExisting.transition, transition),
|
|
4234
|
+
"DUPLICATE_IDENTIFIER",
|
|
4235
|
+
"Purchase-cap transition ID was reused with different facts"
|
|
4236
|
+
);
|
|
4237
|
+
return resultWithUsage(
|
|
4238
|
+
payerState,
|
|
4239
|
+
householdState,
|
|
4240
|
+
current,
|
|
4241
|
+
false,
|
|
4242
|
+
false,
|
|
4243
|
+
transition.occurredAt,
|
|
4244
|
+
policy
|
|
2521
4245
|
);
|
|
2522
4246
|
}
|
|
4247
|
+
economyAssert(
|
|
4248
|
+
expectedPayerVersion === payerState.version && expectedHouseholdVersion === householdState.version,
|
|
4249
|
+
"INVALID_CONTRACT",
|
|
4250
|
+
"Purchase-cap transition has a stale aggregate version"
|
|
4251
|
+
);
|
|
4252
|
+
const replacement = projectReservation(bindingFromReservation(current), [
|
|
4253
|
+
...current.transitionReceipts,
|
|
4254
|
+
{ schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
|
|
4255
|
+
]);
|
|
4256
|
+
const stateChanged = replacement.status !== current.status || replacement.finalTransitionId !== current.finalTransitionId || replacement.finalizedAt !== current.finalizedAt;
|
|
4257
|
+
const nextPayer = replaceReservation(payerState, replacement);
|
|
4258
|
+
const nextHousehold = replaceReservation(householdState, replacement);
|
|
4259
|
+
return resultWithUsage(
|
|
4260
|
+
nextPayer,
|
|
4261
|
+
nextHousehold,
|
|
4262
|
+
replacement,
|
|
4263
|
+
true,
|
|
4264
|
+
stateChanged,
|
|
4265
|
+
transition.occurredAt,
|
|
4266
|
+
policy
|
|
4267
|
+
);
|
|
2523
4268
|
}
|
|
2524
|
-
|
|
4269
|
+
|
|
4270
|
+
// src/purchase-intents.ts
|
|
4271
|
+
var TRANSITION_TYPES = [
|
|
4272
|
+
"checkout-bound",
|
|
4273
|
+
"payment-observed",
|
|
4274
|
+
"credit-recorded",
|
|
4275
|
+
"expire",
|
|
4276
|
+
"cancel",
|
|
4277
|
+
"dispute-opened",
|
|
4278
|
+
"dispute-won",
|
|
4279
|
+
"dispute-lost"
|
|
4280
|
+
];
|
|
4281
|
+
var TRANSITION_PRECEDENCE2 = {
|
|
4282
|
+
"checkout-bound": 0,
|
|
4283
|
+
"payment-observed": 1,
|
|
4284
|
+
"credit-recorded": 2,
|
|
4285
|
+
"dispute-opened": 3,
|
|
4286
|
+
"dispute-won": 4,
|
|
4287
|
+
"dispute-lost": 4,
|
|
4288
|
+
cancel: 5,
|
|
4289
|
+
expire: 6
|
|
4290
|
+
};
|
|
4291
|
+
function assertCheckoutHash(value) {
|
|
2525
4292
|
economyAssert(
|
|
2526
|
-
|
|
4293
|
+
/^sha256:[a-f0-9]{64}$/u.test(value),
|
|
2527
4294
|
"INVALID_CONTRACT",
|
|
2528
|
-
"
|
|
4295
|
+
"Checkout references must be stored as sanitized SHA-256 hashes"
|
|
2529
4296
|
);
|
|
2530
|
-
|
|
2531
|
-
|
|
4297
|
+
}
|
|
4298
|
+
function assertPurchaseIntentTransition(transition) {
|
|
2532
4299
|
economyAssert(
|
|
2533
|
-
|
|
4300
|
+
transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2534
4301
|
"INVALID_CONTRACT",
|
|
2535
|
-
"
|
|
4302
|
+
"Unsupported purchase-intent transition contract version"
|
|
2536
4303
|
);
|
|
2537
|
-
|
|
4304
|
+
assertEconomyIdentifier(transition.transitionId, "transitionId");
|
|
4305
|
+
assertEconomyIdentifier(transition.intentId, "intentId");
|
|
2538
4306
|
economyAssert(
|
|
2539
|
-
|
|
2540
|
-
"
|
|
2541
|
-
"
|
|
4307
|
+
TRANSITION_TYPES.includes(transition.transitionType),
|
|
4308
|
+
"INVALID_CONTRACT",
|
|
4309
|
+
"Purchase-intent transition type is unsupported"
|
|
2542
4310
|
);
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
"beneficiaryAccountId"
|
|
2547
|
-
);
|
|
4311
|
+
parseIsoTimestamp(transition.occurredAt);
|
|
4312
|
+
if (transition.providerCheckoutReferenceHash !== void 0) {
|
|
4313
|
+
assertCheckoutHash(transition.providerCheckoutReferenceHash);
|
|
2548
4314
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
4315
|
+
const requiresCheckoutHash = transition.transitionType === "checkout-bound" || transition.transitionType === "payment-observed";
|
|
4316
|
+
economyAssert(
|
|
4317
|
+
requiresCheckoutHash === (transition.providerCheckoutReferenceHash !== void 0),
|
|
4318
|
+
"INVALID_CONTRACT",
|
|
4319
|
+
"Only checkout and payment transitions must carry a checkout binding"
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
function sameTransition2(left, right) {
|
|
4323
|
+
return left.schemaVersion === right.schemaVersion && left.transitionId === right.transitionId && left.intentId === right.intentId && left.transitionType === right.transitionType && left.occurredAt === right.occurredAt && left.providerCheckoutReferenceHash === right.providerCheckoutReferenceHash;
|
|
4324
|
+
}
|
|
4325
|
+
function sameIntent(left, right) {
|
|
4326
|
+
return left.schemaVersion === right.schemaVersion && left.intentId === right.intentId && left.payerAccountId === right.payerAccountId && left.receivingHouseholdId === right.receivingHouseholdId && left.receivingWalletId === right.receivingWalletId && left.packId === right.packId && left.catalogVersion === right.catalogVersion && left.expectedCurrency === right.expectedCurrency && left.expectedPriceMinorUnits === right.expectedPriceMinorUnits && left.grantAmount === right.grantAmount && left.status === right.status && left.createdAt === right.createdAt && left.expiresAt === right.expiresAt && left.providerCheckoutReferenceHash === right.providerCheckoutReferenceHash;
|
|
4327
|
+
}
|
|
4328
|
+
function compareTransitions2(left, right) {
|
|
4329
|
+
const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
|
|
4330
|
+
if (timeDifference !== 0) {
|
|
4331
|
+
return timeDifference;
|
|
2551
4332
|
}
|
|
2552
|
-
|
|
4333
|
+
const precedenceDifference = TRANSITION_PRECEDENCE2[left.transitionType] - TRANSITION_PRECEDENCE2[right.transitionType];
|
|
4334
|
+
return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
|
|
4335
|
+
}
|
|
4336
|
+
function assertMatchingCheckoutBinding(intent, checkoutHash) {
|
|
2553
4337
|
economyAssert(
|
|
2554
|
-
|
|
4338
|
+
intent.providerCheckoutReferenceHash === void 0 || intent.providerCheckoutReferenceHash === checkoutHash,
|
|
2555
4339
|
"INVALID_CONTRACT",
|
|
2556
|
-
"
|
|
4340
|
+
"Purchase-intent transition conflicts with its checkout binding"
|
|
2557
4341
|
);
|
|
2558
|
-
|
|
2559
|
-
|
|
4342
|
+
}
|
|
4343
|
+
function projectPurchaseIntent(initialIntent, transitions, pack) {
|
|
4344
|
+
assertPurchaseIntentBinding(initialIntent, pack);
|
|
4345
|
+
economyAssert(
|
|
4346
|
+
initialIntent.status === "created" && initialIntent.providerCheckoutReferenceHash === void 0,
|
|
4347
|
+
"INVALID_CONTRACT",
|
|
4348
|
+
"Purchase-intent lifecycle requires immutable created intent facts"
|
|
4349
|
+
);
|
|
4350
|
+
const createdAt = parseIsoTimestamp(initialIntent.createdAt);
|
|
4351
|
+
const expiresAt = parseIsoTimestamp(initialIntent.expiresAt);
|
|
4352
|
+
let intent = initialIntent;
|
|
4353
|
+
let disputeDisposition = "none";
|
|
4354
|
+
let creditTransitionId;
|
|
4355
|
+
const effectiveTransitionIds = [];
|
|
4356
|
+
for (const transition of [...transitions].sort(compareTransitions2)) {
|
|
4357
|
+
assertPurchaseIntentTransition(transition);
|
|
2560
4358
|
economyAssert(
|
|
2561
|
-
|
|
4359
|
+
transition.intentId === initialIntent.intentId,
|
|
2562
4360
|
"INVALID_CONTRACT",
|
|
2563
|
-
"
|
|
4361
|
+
"Purchase-intent transition belongs to another intent"
|
|
2564
4362
|
);
|
|
2565
|
-
|
|
2566
|
-
assertEconomyIdentifier(entry.commandId, "commandId");
|
|
4363
|
+
const eventTime = parseIsoTimestamp(transition.occurredAt);
|
|
2567
4364
|
economyAssert(
|
|
2568
|
-
|
|
2569
|
-
"
|
|
2570
|
-
"
|
|
4365
|
+
eventTime >= createdAt,
|
|
4366
|
+
"INVALID_TIME_WINDOW",
|
|
4367
|
+
"Purchase-intent transition cannot precede intent creation"
|
|
2571
4368
|
);
|
|
4369
|
+
let effective = false;
|
|
4370
|
+
if (transition.transitionType === "checkout-bound") {
|
|
4371
|
+
const checkoutHash = transition.providerCheckoutReferenceHash;
|
|
4372
|
+
assertMatchingCheckoutBinding(intent, checkoutHash);
|
|
4373
|
+
if (intent.status === "created") {
|
|
4374
|
+
economyAssert(
|
|
4375
|
+
eventTime < expiresAt,
|
|
4376
|
+
"INVALID_TIME_WINDOW",
|
|
4377
|
+
"Checkout cannot be bound after purchase-intent expiry"
|
|
4378
|
+
);
|
|
4379
|
+
intent = {
|
|
4380
|
+
...intent,
|
|
4381
|
+
status: "checkout-created",
|
|
4382
|
+
providerCheckoutReferenceHash: checkoutHash
|
|
4383
|
+
};
|
|
4384
|
+
effective = true;
|
|
4385
|
+
} else {
|
|
4386
|
+
economyAssert(
|
|
4387
|
+
!["expired", "cancelled"].includes(intent.status),
|
|
4388
|
+
"INVALID_CONTRACT",
|
|
4389
|
+
"A terminal purchase intent cannot bind a checkout"
|
|
4390
|
+
);
|
|
4391
|
+
}
|
|
4392
|
+
} else if (transition.transitionType === "payment-observed") {
|
|
4393
|
+
const checkoutHash = transition.providerCheckoutReferenceHash;
|
|
4394
|
+
assertMatchingCheckoutBinding(intent, checkoutHash);
|
|
4395
|
+
economyAssert(
|
|
4396
|
+
eventTime < expiresAt,
|
|
4397
|
+
"INVALID_TIME_WINDOW",
|
|
4398
|
+
"Authoritative payment time must precede purchase-intent expiry"
|
|
4399
|
+
);
|
|
4400
|
+
if (intent.status === "created") {
|
|
4401
|
+
intent = {
|
|
4402
|
+
...intent,
|
|
4403
|
+
status: "paid-unreconciled",
|
|
4404
|
+
providerCheckoutReferenceHash: checkoutHash
|
|
4405
|
+
};
|
|
4406
|
+
effective = true;
|
|
4407
|
+
} else if (intent.status === "checkout-created") {
|
|
4408
|
+
intent = { ...intent, status: "paid-unreconciled" };
|
|
4409
|
+
effective = true;
|
|
4410
|
+
} else {
|
|
4411
|
+
economyAssert(
|
|
4412
|
+
!["expired", "cancelled"].includes(intent.status),
|
|
4413
|
+
"INVALID_CONTRACT",
|
|
4414
|
+
"A terminal purchase intent cannot observe payment"
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
} else if (transition.transitionType === "credit-recorded") {
|
|
4418
|
+
if (intent.status === "paid-unreconciled") {
|
|
4419
|
+
intent = { ...intent, status: "credited" };
|
|
4420
|
+
disputeDisposition = "none";
|
|
4421
|
+
creditTransitionId = transition.transitionId;
|
|
4422
|
+
effective = true;
|
|
4423
|
+
} else {
|
|
4424
|
+
economyAssert(
|
|
4425
|
+
intent.status === "credited" || intent.status === "disputed",
|
|
4426
|
+
"INVALID_CONTRACT",
|
|
4427
|
+
"Purchase intent cannot be credited before authoritative payment"
|
|
4428
|
+
);
|
|
4429
|
+
}
|
|
4430
|
+
} else if (transition.transitionType === "dispute-opened") {
|
|
4431
|
+
if (intent.status === "credited" && disputeDisposition === "none") {
|
|
4432
|
+
intent = { ...intent, status: "disputed" };
|
|
4433
|
+
disputeDisposition = "open";
|
|
4434
|
+
effective = true;
|
|
4435
|
+
} else {
|
|
4436
|
+
economyAssert(
|
|
4437
|
+
intent.status === "disputed" && disputeDisposition === "open",
|
|
4438
|
+
"INVALID_CONTRACT",
|
|
4439
|
+
"Dispute can open only once after purchase credit"
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
} else if (transition.transitionType === "dispute-won") {
|
|
4443
|
+
if (intent.status === "disputed" && disputeDisposition === "open") {
|
|
4444
|
+
intent = { ...intent, status: "credited" };
|
|
4445
|
+
disputeDisposition = "won";
|
|
4446
|
+
effective = true;
|
|
4447
|
+
} else {
|
|
4448
|
+
economyAssert(
|
|
4449
|
+
intent.status === "credited" && disputeDisposition === "won",
|
|
4450
|
+
"INVALID_CONTRACT",
|
|
4451
|
+
"Dispute-win transition requires one open dispute"
|
|
4452
|
+
);
|
|
4453
|
+
}
|
|
4454
|
+
} else if (transition.transitionType === "dispute-lost") {
|
|
4455
|
+
if (intent.status === "disputed" && disputeDisposition === "open") {
|
|
4456
|
+
disputeDisposition = "lost";
|
|
4457
|
+
effective = true;
|
|
4458
|
+
} else {
|
|
4459
|
+
economyAssert(
|
|
4460
|
+
intent.status === "disputed" && disputeDisposition === "lost",
|
|
4461
|
+
"INVALID_CONTRACT",
|
|
4462
|
+
"Dispute-loss transition requires one open dispute"
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
} else if (transition.transitionType === "cancel") {
|
|
4466
|
+
if (intent.status === "created" || intent.status === "checkout-created") {
|
|
4467
|
+
intent = { ...intent, status: "cancelled" };
|
|
4468
|
+
effective = true;
|
|
4469
|
+
}
|
|
4470
|
+
} else {
|
|
4471
|
+
economyAssert(
|
|
4472
|
+
eventTime >= expiresAt,
|
|
4473
|
+
"INVALID_TIME_WINDOW",
|
|
4474
|
+
"Expiry transition cannot precede purchase-intent expiry"
|
|
4475
|
+
);
|
|
4476
|
+
if (intent.status === "created" || intent.status === "checkout-created") {
|
|
4477
|
+
intent = { ...intent, status: "expired" };
|
|
4478
|
+
effective = true;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
if (effective) {
|
|
4482
|
+
effectiveTransitionIds.push(transition.transitionId);
|
|
4483
|
+
}
|
|
2572
4484
|
}
|
|
4485
|
+
return {
|
|
4486
|
+
intent,
|
|
4487
|
+
disputeDisposition,
|
|
4488
|
+
effectiveTransitionIds,
|
|
4489
|
+
...creditTransitionId === void 0 ? {} : { creditTransitionId }
|
|
4490
|
+
};
|
|
2573
4491
|
}
|
|
2574
|
-
function
|
|
4492
|
+
function assertPurchaseIntentLifecycle(lifecycle, pack) {
|
|
2575
4493
|
economyAssert(
|
|
2576
|
-
|
|
4494
|
+
lifecycle.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
2577
4495
|
"INVALID_CONTRACT",
|
|
2578
|
-
"
|
|
4496
|
+
"Unsupported purchase-intent lifecycle contract version"
|
|
2579
4497
|
);
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
assertEconomyIdentifier(
|
|
2598
|
-
filter.beneficiaryAccountId,
|
|
2599
|
-
"beneficiaryAccountId"
|
|
4498
|
+
economyAssert(
|
|
4499
|
+
Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
|
|
4500
|
+
"INVALID_CONTRACT",
|
|
4501
|
+
"Purchase-intent lifecycle version must match its receipt count"
|
|
4502
|
+
);
|
|
4503
|
+
const transitionIds = /* @__PURE__ */ new Set();
|
|
4504
|
+
for (const receipt of lifecycle.receipts) {
|
|
4505
|
+
economyAssert(
|
|
4506
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
4507
|
+
"INVALID_CONTRACT",
|
|
4508
|
+
"Purchase-intent receipt is invalid"
|
|
4509
|
+
);
|
|
4510
|
+
assertPurchaseIntentTransition(receipt.transition);
|
|
4511
|
+
economyAssert(
|
|
4512
|
+
receipt.transition.intentId === lifecycle.initialIntent.intentId && !transitionIds.has(receipt.transition.transitionId),
|
|
4513
|
+
"DUPLICATE_IDENTIFIER",
|
|
4514
|
+
"Purchase-intent receipts must be unique and belong to the intent"
|
|
2600
4515
|
);
|
|
4516
|
+
transitionIds.add(receipt.transition.transitionId);
|
|
2601
4517
|
}
|
|
2602
|
-
const
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
"INVALID_TIME_WINDOW",
|
|
2607
|
-
"Activity filter start must precede its exclusive end"
|
|
4518
|
+
const projection = projectPurchaseIntent(
|
|
4519
|
+
lifecycle.initialIntent,
|
|
4520
|
+
lifecycle.receipts.map((receipt) => receipt.transition),
|
|
4521
|
+
pack
|
|
2608
4522
|
);
|
|
2609
|
-
}
|
|
2610
|
-
function assertWalletActivityPage(page) {
|
|
2611
4523
|
economyAssert(
|
|
2612
|
-
|
|
4524
|
+
sameIntent(projection.intent, lifecycle.intent) && lifecycle.creditRecorded === (projection.intent.status === "credited" || projection.intent.status === "disputed") && lifecycle.disputeDisposition === projection.disputeDisposition && lifecycle.effectiveTransitionIds.length === projection.effectiveTransitionIds.length && lifecycle.effectiveTransitionIds.every(
|
|
4525
|
+
(transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
|
|
4526
|
+
),
|
|
2613
4527
|
"INVALID_CONTRACT",
|
|
2614
|
-
"
|
|
4528
|
+
"Purchase-intent lifecycle does not match its immutable event rebuild"
|
|
2615
4529
|
);
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
4530
|
+
}
|
|
4531
|
+
function createPurchaseIntentLifecycle(initialIntent, pack) {
|
|
4532
|
+
const projection = projectPurchaseIntent(initialIntent, [], pack);
|
|
4533
|
+
return {
|
|
4534
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
4535
|
+
initialIntent,
|
|
4536
|
+
intent: projection.intent,
|
|
4537
|
+
version: 1,
|
|
4538
|
+
creditRecorded: false,
|
|
4539
|
+
disputeDisposition: projection.disputeDisposition,
|
|
4540
|
+
receipts: [],
|
|
4541
|
+
effectiveTransitionIds: []
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4544
|
+
function sameProjection(lifecycle, projection) {
|
|
4545
|
+
return sameIntent(lifecycle.intent, projection.intent) && lifecycle.disputeDisposition === projection.disputeDisposition && lifecycle.effectiveTransitionIds.length === projection.effectiveTransitionIds.length && lifecycle.effectiveTransitionIds.every(
|
|
4546
|
+
(transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
|
|
2620
4547
|
);
|
|
4548
|
+
}
|
|
4549
|
+
function applyPurchaseIntentTransition(lifecycle, transition, expectedVersion, pack) {
|
|
4550
|
+
assertPurchaseIntentLifecycle(lifecycle, pack);
|
|
4551
|
+
assertPurchaseIntentTransition(transition);
|
|
2621
4552
|
economyAssert(
|
|
2622
|
-
|
|
4553
|
+
transition.intentId === lifecycle.initialIntent.intentId,
|
|
2623
4554
|
"INVALID_CONTRACT",
|
|
2624
|
-
"
|
|
4555
|
+
"Purchase-intent transition belongs to another intent"
|
|
2625
4556
|
);
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
let previous;
|
|
2631
|
-
for (const entry of page.entries) {
|
|
2632
|
-
assertWalletActivityEntry(entry);
|
|
4557
|
+
const replay = lifecycle.receipts.find(
|
|
4558
|
+
(receipt) => receipt.transition.transitionId === transition.transitionId
|
|
4559
|
+
);
|
|
4560
|
+
if (replay !== void 0) {
|
|
2633
4561
|
economyAssert(
|
|
2634
|
-
|
|
4562
|
+
sameTransition2(replay.transition, transition),
|
|
2635
4563
|
"DUPLICATE_IDENTIFIER",
|
|
2636
|
-
"
|
|
4564
|
+
"Purchase-intent transition ID was reused with different facts"
|
|
2637
4565
|
);
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
4566
|
+
return { lifecycle, recorded: false, stateChanged: false };
|
|
4567
|
+
}
|
|
4568
|
+
economyAssert(
|
|
4569
|
+
expectedVersion === lifecycle.version,
|
|
4570
|
+
"INVALID_CONTRACT",
|
|
4571
|
+
"Purchase-intent transition has a stale expected version"
|
|
4572
|
+
);
|
|
4573
|
+
const receipts = [
|
|
4574
|
+
...lifecycle.receipts,
|
|
4575
|
+
{ schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
|
|
4576
|
+
];
|
|
4577
|
+
const projection = projectPurchaseIntent(
|
|
4578
|
+
lifecycle.initialIntent,
|
|
4579
|
+
receipts.map((receipt) => receipt.transition),
|
|
4580
|
+
pack
|
|
4581
|
+
);
|
|
4582
|
+
const stateChanged = !sameProjection(lifecycle, projection);
|
|
4583
|
+
const nextLifecycle = {
|
|
4584
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
4585
|
+
initialIntent: lifecycle.initialIntent,
|
|
4586
|
+
intent: projection.intent,
|
|
4587
|
+
version: lifecycle.version + 1,
|
|
4588
|
+
creditRecorded: projection.intent.status === "credited" || projection.intent.status === "disputed",
|
|
4589
|
+
disputeDisposition: projection.disputeDisposition,
|
|
4590
|
+
receipts,
|
|
4591
|
+
effectiveTransitionIds: projection.effectiveTransitionIds
|
|
4592
|
+
};
|
|
4593
|
+
assertPurchaseIntentLifecycle(nextLifecycle, pack);
|
|
4594
|
+
const producesCredit = projection.creditTransitionId === transition.transitionId && !lifecycle.creditRecorded && nextLifecycle.creditRecorded;
|
|
4595
|
+
const creditInstruction = producesCredit ? {
|
|
4596
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
4597
|
+
intentId: nextLifecycle.intent.intentId,
|
|
4598
|
+
transitionId: transition.transitionId,
|
|
4599
|
+
idempotencyKey: nextLifecycle.intent.intentId
|
|
4600
|
+
} : void 0;
|
|
4601
|
+
return {
|
|
4602
|
+
lifecycle: nextLifecycle,
|
|
4603
|
+
recorded: true,
|
|
4604
|
+
stateChanged,
|
|
4605
|
+
...creditInstruction === void 0 ? {} : { creditInstruction }
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
function reducePurchaseIntentTransitions(initialIntent, transitions, pack) {
|
|
4609
|
+
const unique = /* @__PURE__ */ new Map();
|
|
4610
|
+
for (const transition of transitions) {
|
|
4611
|
+
assertPurchaseIntentTransition(transition);
|
|
4612
|
+
const existing = unique.get(transition.transitionId);
|
|
4613
|
+
if (existing !== void 0) {
|
|
2642
4614
|
economyAssert(
|
|
2643
|
-
|
|
2644
|
-
"
|
|
2645
|
-
"
|
|
4615
|
+
sameTransition2(existing, transition),
|
|
4616
|
+
"DUPLICATE_IDENTIFIER",
|
|
4617
|
+
"Purchase-intent transition ID was reused with different facts"
|
|
2646
4618
|
);
|
|
4619
|
+
} else {
|
|
4620
|
+
unique.set(transition.transitionId, transition);
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
let lifecycle = createPurchaseIntentLifecycle(initialIntent, pack);
|
|
4624
|
+
const creditInstructions = [];
|
|
4625
|
+
for (const transition of [...unique.values()].sort(compareTransitions2)) {
|
|
4626
|
+
const result = applyPurchaseIntentTransition(
|
|
4627
|
+
lifecycle,
|
|
4628
|
+
transition,
|
|
4629
|
+
lifecycle.version,
|
|
4630
|
+
pack
|
|
4631
|
+
);
|
|
4632
|
+
lifecycle = result.lifecycle;
|
|
4633
|
+
if (result.creditInstruction !== void 0) {
|
|
4634
|
+
creditInstructions.push(result.creditInstruction);
|
|
2647
4635
|
}
|
|
2648
|
-
previous = entry;
|
|
2649
4636
|
}
|
|
2650
|
-
}
|
|
2651
|
-
function assertWalletActivityPageForPortfolio(page, scope) {
|
|
2652
|
-
assertWalletActivityPage(page);
|
|
2653
|
-
assertWalletPortfolioReadScope(scope);
|
|
2654
|
-
const walletIds = new Set(scope.components.map((item) => item.walletId));
|
|
2655
4637
|
economyAssert(
|
|
2656
|
-
|
|
2657
|
-
"
|
|
2658
|
-
"
|
|
4638
|
+
creditInstructions.length <= 1,
|
|
4639
|
+
"DUPLICATE_TRANSACTION",
|
|
4640
|
+
"Purchase-intent reduction produced more than one credit"
|
|
2659
4641
|
);
|
|
4642
|
+
return { lifecycle, creditInstructions };
|
|
2660
4643
|
}
|
|
2661
4644
|
|
|
2662
4645
|
// src/recurrence.ts
|
|
@@ -2791,11 +4774,14 @@ export {
|
|
|
2791
4774
|
EARLY_BACKER_SETTLEMENT_POLICY_V2,
|
|
2792
4775
|
ECONOMY_CONTRACT_VERSION,
|
|
2793
4776
|
EconomyError,
|
|
4777
|
+
MODULE_ALLOWANCE_PURPOSE,
|
|
2794
4778
|
PRE_UTILITY_BACKER_COHORT,
|
|
2795
4779
|
TOKEN_SUBUNITS_MAX,
|
|
2796
4780
|
TOKEN_SUBUNITS_MIN,
|
|
2797
4781
|
TOKEN_SUBUNITS_PER_TOKEN,
|
|
2798
4782
|
advanceJournalChainHead,
|
|
4783
|
+
applyPaidLotLifecycleEvent,
|
|
4784
|
+
applyPurchaseIntentTransition,
|
|
2799
4785
|
applySourceLotMovement,
|
|
2800
4786
|
applyTransactionToProjection,
|
|
2801
4787
|
applyWalletBalanceDelta,
|
|
@@ -2813,13 +4799,26 @@ export {
|
|
|
2813
4799
|
assertFxSnapshot,
|
|
2814
4800
|
assertGameplayAllocation,
|
|
2815
4801
|
assertJournalChainHead,
|
|
4802
|
+
assertModuleAllowance,
|
|
4803
|
+
assertModulePurchaseReceipt,
|
|
4804
|
+
assertModulePurchaseReconciliationObservation,
|
|
4805
|
+
assertModuleSpendHold,
|
|
4806
|
+
assertModuleSpendQuote,
|
|
2816
4807
|
assertNonNegativeAccounts,
|
|
2817
4808
|
assertOpenPurchaseIntent,
|
|
4809
|
+
assertPaidLotLifecycle,
|
|
4810
|
+
assertPaidLotLifecycleEvent,
|
|
4811
|
+
assertPaidLotPurchaseProvenance,
|
|
4812
|
+
assertPurchaseCapReservation,
|
|
4813
|
+
assertPurchaseCapReservationTransition,
|
|
2818
4814
|
assertPurchaseIntentBinding,
|
|
4815
|
+
assertPurchaseIntentLifecycle,
|
|
4816
|
+
assertPurchaseIntentTransition,
|
|
2819
4817
|
assertPurchaseLimitPolicy,
|
|
2820
4818
|
assertReversalAvailable,
|
|
2821
4819
|
assertRewardConversion,
|
|
2822
4820
|
assertRewardRate,
|
|
4821
|
+
assertRollingPurchaseCapState,
|
|
2823
4822
|
assertScopedPersistedIdempotencyResult,
|
|
2824
4823
|
assertSourceLot,
|
|
2825
4824
|
assertSourceLotMovement,
|
|
@@ -2845,13 +4844,23 @@ export {
|
|
|
2845
4844
|
assertWalletPortfolioSummary,
|
|
2846
4845
|
availableSourceLotAmount,
|
|
2847
4846
|
boostGameplayAllocation,
|
|
4847
|
+
calculateRollingPurchaseCapUsage,
|
|
2848
4848
|
canUseSourceLot,
|
|
2849
4849
|
canonicalTransactionPayload,
|
|
4850
|
+
closeModuleAllowance,
|
|
2850
4851
|
convertRewardPayout,
|
|
4852
|
+
createEarlyBackerRetentionFromPaidLot,
|
|
2851
4853
|
createGameplayAllocation,
|
|
2852
4854
|
createInitialSourceLotSnapshot,
|
|
4855
|
+
createModuleAllowance,
|
|
4856
|
+
createModulePurchaseReceipt,
|
|
4857
|
+
createModuleSpendHold,
|
|
4858
|
+
createModuleSpendQuote,
|
|
4859
|
+
createPaidLotLifecycle,
|
|
2853
4860
|
createProjectionSnapshot,
|
|
4861
|
+
createPurchaseIntentLifecycle,
|
|
2854
4862
|
createReversalTransaction,
|
|
4863
|
+
createRollingPurchaseCapState,
|
|
2855
4864
|
createWalletBalanceSummary,
|
|
2856
4865
|
createWalletPortfolioLifetime,
|
|
2857
4866
|
createWalletPortfolioSummary,
|
|
@@ -2860,18 +4869,29 @@ export {
|
|
|
2860
4869
|
economyAssert,
|
|
2861
4870
|
evaluateEarlyBacker,
|
|
2862
4871
|
evaluateEarlyBackerBySettlementV2,
|
|
4872
|
+
fundModuleAllowance,
|
|
2863
4873
|
isWholeTokenAmount,
|
|
2864
4874
|
mapBitLabsConversionState,
|
|
4875
|
+
parseGbpMinorUnits,
|
|
2865
4876
|
parseIsoTimestamp,
|
|
2866
4877
|
parseTokenSubunits,
|
|
2867
4878
|
rebuildBalanceProjection,
|
|
2868
4879
|
reclaimGameplayAllocation,
|
|
4880
|
+
reclaimModuleAllowance,
|
|
4881
|
+
reconcileModulePurchase,
|
|
4882
|
+
reducePaidLotLifecycleEvents,
|
|
4883
|
+
reducePurchaseIntentTransitions,
|
|
4884
|
+
releaseModuleSpendHold,
|
|
4885
|
+
reserveRollingPurchaseCaps,
|
|
2869
4886
|
selectSourceLots,
|
|
4887
|
+
serializeGbpMinorUnits,
|
|
2870
4888
|
serializeTokenSubunits,
|
|
4889
|
+
settleModuleSpendHold,
|
|
2871
4890
|
sortStringRecord,
|
|
2872
4891
|
subunitsToWholeTokens,
|
|
2873
4892
|
sumTokenSubunits,
|
|
2874
4893
|
toEconomyErrorEnvelope,
|
|
4894
|
+
transitionRollingPurchaseCapReservation,
|
|
2875
4895
|
wholeTokensToSubunits
|
|
2876
4896
|
};
|
|
2877
4897
|
//# sourceMappingURL=index.js.map
|