@plasius/economy 0.3.2 → 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/dist/index.js CHANGED
@@ -1546,6 +1546,682 @@ function selectSourceLots(lots, requestedAmount, context) {
1546
1546
  );
1547
1547
  }
1548
1548
 
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`);
1554
+ economyAssert(
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,
1580
+ "INVALID_CONTRACT",
1581
+ `${label} must sum to the operation amount`
1582
+ );
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) {
1595
+ economyAssert(
1596
+ typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value),
1597
+ "INVALID_CONTRACT",
1598
+ `${label} must be a canonical SHA-256 reference`
1599
+ );
1600
+ }
1601
+ function assertUpdatedAt(current, occurredAt, label) {
1602
+ economyAssert(
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,
1611
+ "INVALID_CONTRACT",
1612
+ "Unsupported Module Allowance contract"
1613
+ );
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");
1619
+ economyAssert(
1620
+ allowance.hostWalletId !== allowance.allowanceWalletId,
1621
+ "INVALID_CONTRACT",
1622
+ "Host and Module Allowance wallets must be distinct"
1623
+ );
1624
+ economyAssert(
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,
1633
+ "INVALID_TIME_WINDOW",
1634
+ "Module Allowance update cannot precede creation"
1635
+ );
1636
+ economyAssert(
1637
+ Number.isSafeInteger(allowance.version) && allowance.version >= 1,
1638
+ "INVALID_CONTRACT",
1639
+ "Module Allowance version must be a positive safe integer"
1640
+ );
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);
1646
+ economyAssert(
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"
1652
+ );
1653
+ economyAssert(
1654
+ allocated === available + held + spent + reclaimed,
1655
+ "INVALID_CONTRACT",
1656
+ "Module Allowance buckets must equal its lifetime allocation"
1657
+ );
1658
+ economyAssert(
1659
+ allowance.status !== "closed" || available === 0n && held === 0n,
1660
+ "INVALID_CONTRACT",
1661
+ "A closed Module Allowance cannot retain available or held value"
1662
+ );
1663
+ const fundedByLot = sumSlicesByLot(allowance.fundingSlices);
1664
+ const reclaimedByLot = sumSlicesByLot(allowance.reclaimedSlices);
1665
+ economyAssert(
1666
+ sumTokenSubunits(allowance.fundingSlices.map((slice) => slice.amount)) === allocated,
1667
+ "INVALID_CONTRACT",
1668
+ "Module Allowance funding provenance must equal allocated value"
1669
+ );
1670
+ economyAssert(
1671
+ sumTokenSubunits(allowance.reclaimedSlices.map((slice) => slice.amount)) === reclaimed,
1672
+ "INVALID_CONTRACT",
1673
+ "Module Allowance reclaim provenance must equal reclaimed value"
1674
+ );
1675
+ for (const [lotId, amount] of reclaimedByLot) {
1676
+ economyAssert(
1677
+ amount <= (fundedByLot.get(lotId) ?? 0n),
1678
+ "SOURCE_LOT_RESTRICTED",
1679
+ "Reclaimed provenance cannot exceed Module Allowance funding"
1680
+ );
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);
1711
+ economyAssert(
1712
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1713
+ "INVALID_CONTRACT",
1714
+ "Module Allowance is closed or its optimistic version is stale"
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);
1736
+ economyAssert(
1737
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1738
+ "INVALID_CONTRACT",
1739
+ "Module Allowance is closed or its optimistic version is stale"
1740
+ );
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"
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;
1771
+ }
1772
+ function closeModuleAllowance(allowance, input) {
1773
+ assertModuleAllowance(allowance);
1774
+ economyAssert(
1775
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1776
+ "INVALID_CONTRACT",
1777
+ "Module Allowance is closed or its optimistic version is stale"
1778
+ );
1779
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Closure time");
1780
+ economyAssert(
1781
+ parseTokenSubunits(allowance.availableAmount) === 0n && parseTokenSubunits(allowance.heldAmount) === 0n,
1782
+ "INVALID_CONTRACT",
1783
+ "Available value must be reclaimed and holds resolved before closure"
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) {
1795
+ economyAssert(
1796
+ quote.schemaVersion === ECONOMY_CONTRACT_VERSION,
1797
+ "INVALID_CONTRACT",
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"
1823
+ );
1824
+ }
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) {
1834
+ economyAssert(
1835
+ hold.schemaVersion === ECONOMY_CONTRACT_VERSION,
1836
+ "INVALID_CONTRACT",
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"
1849
+ );
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") {
1876
+ economyAssert(
1877
+ hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.releaseTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseIdempotencyKey === void 0,
1878
+ "INVALID_CONTRACT",
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"
1895
+ );
1896
+ } else {
1897
+ economyAssert(
1898
+ hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseTransactionId !== void 0 && hold.releaseIdempotencyKey !== void 0,
1899
+ "INVALID_CONTRACT",
1900
+ "A released hold requires release evidence only"
1901
+ );
1902
+ assertEconomyIdentifier(hold.releaseTransactionId, "releaseTransactionId");
1903
+ assertEconomyIdentifier(hold.releaseIdempotencyKey, "releaseIdempotencyKey");
1904
+ }
1905
+ }
1906
+ function assertAllowanceMatchesQuote(allowance, quote) {
1907
+ economyAssert(
1908
+ allowance.status === "active" && allowance.allowanceId === quote.allowanceId && allowance.householdId === quote.householdId && allowance.childAccountId === quote.childAccountId,
1909
+ "INVALID_CONTRACT",
1910
+ "Module quote does not belong to this active child allowance"
1911
+ );
1912
+ }
1913
+ function assertAllowanceMatchesHold(allowance, hold) {
1914
+ economyAssert(
1915
+ allowance.status === "active" && allowance.allowanceId === hold.allowanceId && allowance.householdId === hold.householdId && allowance.childAccountId === hold.childAccountId,
1916
+ "INVALID_CONTRACT",
1917
+ "Module hold does not belong to this active child allowance"
1918
+ );
1919
+ }
1920
+ function createModuleSpendHold(allowance, quote, input) {
1921
+ assertModuleAllowance(allowance);
1922
+ assertModuleSpendQuote(quote);
1923
+ assertAllowanceMatchesQuote(allowance, quote);
1924
+ economyAssert(
1925
+ allowance.version === input.expectedAllowanceVersion,
1926
+ "INVALID_CONTRACT",
1927
+ "Module Allowance optimistic version is stale"
1928
+ );
1929
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
1930
+ economyAssert(
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"
1934
+ );
1935
+ const amount = parseTokenSubunits(quote.amount);
1936
+ economyAssert(
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,
1993
+ "INVALID_CONTRACT",
1994
+ "Module hold is final or an optimistic version is stale"
1995
+ );
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 };
2037
+ }
2038
+ function releaseModuleSpendHold(allowance, hold, input) {
2039
+ assertModuleAllowance(allowance);
2040
+ assertModuleSpendHold(hold);
2041
+ assertAllowanceMatchesHold(allowance, hold);
2042
+ economyAssert(
2043
+ hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
2044
+ "INVALID_CONTRACT",
2045
+ "Module hold is final or an optimistic version is stale"
2046
+ );
2047
+ assertEconomyIdentifier(input.releaseTransactionId, "releaseTransactionId");
2048
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2049
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
2050
+ economyAssert(
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,
2087
+ "INVALID_CONTRACT",
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"
2102
+ );
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);
2113
+ }
2114
+ function createModulePurchaseReceipt(quote, hold, input) {
2115
+ assertModuleSpendQuote(quote);
2116
+ assertModuleSpendHold(hold);
2117
+ economyAssert(
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,
2119
+ "INVALID_CONTRACT",
2120
+ "Receipt quote and settled hold evidence do not match"
2121
+ );
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;
2146
+ }
2147
+ function assertModulePurchaseReconciliationObservation(observation) {
2148
+ economyAssert(
2149
+ observation.schemaVersion === ECONOMY_CONTRACT_VERSION,
2150
+ "INVALID_CONTRACT",
2151
+ "Unsupported module-purchase reconciliation contract version"
2152
+ );
2153
+ assertEconomyIdentifier(observation.quoteId, "quoteId");
2154
+ assertEconomyIdentifier(observation.holdId, "holdId");
2155
+ assertEconomyIdentifier(observation.childAccountId, "childAccountId");
2156
+ assertEconomyIdentifier(observation.moduleVersionId, "moduleVersionId");
2157
+ economyAssert(
2158
+ ["missing", "held", "settled", "released"].includes(
2159
+ observation.financialState
2160
+ ),
2161
+ "INVALID_CONTRACT",
2162
+ "Reconciliation financial state is unsupported"
2163
+ );
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
2195
+ );
2196
+ }
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
+ );
2223
+ }
2224
+
1549
2225
  // src/operations.ts
1550
2226
  function assertDistinctAdjustmentApproval(request) {
1551
2227
  economyAssert(
@@ -4098,6 +4774,7 @@ export {
4098
4774
  EARLY_BACKER_SETTLEMENT_POLICY_V2,
4099
4775
  ECONOMY_CONTRACT_VERSION,
4100
4776
  EconomyError,
4777
+ MODULE_ALLOWANCE_PURPOSE,
4101
4778
  PRE_UTILITY_BACKER_COHORT,
4102
4779
  TOKEN_SUBUNITS_MAX,
4103
4780
  TOKEN_SUBUNITS_MIN,
@@ -4122,6 +4799,11 @@ export {
4122
4799
  assertFxSnapshot,
4123
4800
  assertGameplayAllocation,
4124
4801
  assertJournalChainHead,
4802
+ assertModuleAllowance,
4803
+ assertModulePurchaseReceipt,
4804
+ assertModulePurchaseReconciliationObservation,
4805
+ assertModuleSpendHold,
4806
+ assertModuleSpendQuote,
4125
4807
  assertNonNegativeAccounts,
4126
4808
  assertOpenPurchaseIntent,
4127
4809
  assertPaidLotLifecycle,
@@ -4165,10 +4847,15 @@ export {
4165
4847
  calculateRollingPurchaseCapUsage,
4166
4848
  canUseSourceLot,
4167
4849
  canonicalTransactionPayload,
4850
+ closeModuleAllowance,
4168
4851
  convertRewardPayout,
4169
4852
  createEarlyBackerRetentionFromPaidLot,
4170
4853
  createGameplayAllocation,
4171
4854
  createInitialSourceLotSnapshot,
4855
+ createModuleAllowance,
4856
+ createModulePurchaseReceipt,
4857
+ createModuleSpendHold,
4858
+ createModuleSpendQuote,
4172
4859
  createPaidLotLifecycle,
4173
4860
  createProjectionSnapshot,
4174
4861
  createPurchaseIntentLifecycle,
@@ -4182,6 +4869,7 @@ export {
4182
4869
  economyAssert,
4183
4870
  evaluateEarlyBacker,
4184
4871
  evaluateEarlyBackerBySettlementV2,
4872
+ fundModuleAllowance,
4185
4873
  isWholeTokenAmount,
4186
4874
  mapBitLabsConversionState,
4187
4875
  parseGbpMinorUnits,
@@ -4189,12 +4877,16 @@ export {
4189
4877
  parseTokenSubunits,
4190
4878
  rebuildBalanceProjection,
4191
4879
  reclaimGameplayAllocation,
4880
+ reclaimModuleAllowance,
4881
+ reconcileModulePurchase,
4192
4882
  reducePaidLotLifecycleEvents,
4193
4883
  reducePurchaseIntentTransitions,
4884
+ releaseModuleSpendHold,
4194
4885
  reserveRollingPurchaseCaps,
4195
4886
  selectSourceLots,
4196
4887
  serializeGbpMinorUnits,
4197
4888
  serializeTokenSubunits,
4889
+ settleModuleSpendHold,
4198
4890
  sortStringRecord,
4199
4891
  subunitsToWholeTokens,
4200
4892
  sumTokenSubunits,