@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.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  EARLY_BACKER_SETTLEMENT_POLICY_V2: () => EARLY_BACKER_SETTLEMENT_POLICY_V2,
29
29
  ECONOMY_CONTRACT_VERSION: () => ECONOMY_CONTRACT_VERSION,
30
30
  EconomyError: () => EconomyError,
31
+ MODULE_ALLOWANCE_PURPOSE: () => MODULE_ALLOWANCE_PURPOSE,
31
32
  PRE_UTILITY_BACKER_COHORT: () => PRE_UTILITY_BACKER_COHORT,
32
33
  TOKEN_SUBUNITS_MAX: () => TOKEN_SUBUNITS_MAX,
33
34
  TOKEN_SUBUNITS_MIN: () => TOKEN_SUBUNITS_MIN,
@@ -52,6 +53,11 @@ __export(index_exports, {
52
53
  assertFxSnapshot: () => assertFxSnapshot,
53
54
  assertGameplayAllocation: () => assertGameplayAllocation,
54
55
  assertJournalChainHead: () => assertJournalChainHead,
56
+ assertModuleAllowance: () => assertModuleAllowance,
57
+ assertModulePurchaseReceipt: () => assertModulePurchaseReceipt,
58
+ assertModulePurchaseReconciliationObservation: () => assertModulePurchaseReconciliationObservation,
59
+ assertModuleSpendHold: () => assertModuleSpendHold,
60
+ assertModuleSpendQuote: () => assertModuleSpendQuote,
55
61
  assertNonNegativeAccounts: () => assertNonNegativeAccounts,
56
62
  assertOpenPurchaseIntent: () => assertOpenPurchaseIntent,
57
63
  assertPaidLotLifecycle: () => assertPaidLotLifecycle,
@@ -95,10 +101,15 @@ __export(index_exports, {
95
101
  calculateRollingPurchaseCapUsage: () => calculateRollingPurchaseCapUsage,
96
102
  canUseSourceLot: () => canUseSourceLot,
97
103
  canonicalTransactionPayload: () => canonicalTransactionPayload,
104
+ closeModuleAllowance: () => closeModuleAllowance,
98
105
  convertRewardPayout: () => convertRewardPayout,
99
106
  createEarlyBackerRetentionFromPaidLot: () => createEarlyBackerRetentionFromPaidLot,
100
107
  createGameplayAllocation: () => createGameplayAllocation,
101
108
  createInitialSourceLotSnapshot: () => createInitialSourceLotSnapshot,
109
+ createModuleAllowance: () => createModuleAllowance,
110
+ createModulePurchaseReceipt: () => createModulePurchaseReceipt,
111
+ createModuleSpendHold: () => createModuleSpendHold,
112
+ createModuleSpendQuote: () => createModuleSpendQuote,
102
113
  createPaidLotLifecycle: () => createPaidLotLifecycle,
103
114
  createProjectionSnapshot: () => createProjectionSnapshot,
104
115
  createPurchaseIntentLifecycle: () => createPurchaseIntentLifecycle,
@@ -112,6 +123,7 @@ __export(index_exports, {
112
123
  economyAssert: () => economyAssert,
113
124
  evaluateEarlyBacker: () => evaluateEarlyBacker,
114
125
  evaluateEarlyBackerBySettlementV2: () => evaluateEarlyBackerBySettlementV2,
126
+ fundModuleAllowance: () => fundModuleAllowance,
115
127
  isWholeTokenAmount: () => isWholeTokenAmount,
116
128
  mapBitLabsConversionState: () => mapBitLabsConversionState,
117
129
  parseGbpMinorUnits: () => parseGbpMinorUnits,
@@ -119,12 +131,16 @@ __export(index_exports, {
119
131
  parseTokenSubunits: () => parseTokenSubunits,
120
132
  rebuildBalanceProjection: () => rebuildBalanceProjection,
121
133
  reclaimGameplayAllocation: () => reclaimGameplayAllocation,
134
+ reclaimModuleAllowance: () => reclaimModuleAllowance,
135
+ reconcileModulePurchase: () => reconcileModulePurchase,
122
136
  reducePaidLotLifecycleEvents: () => reducePaidLotLifecycleEvents,
123
137
  reducePurchaseIntentTransitions: () => reducePurchaseIntentTransitions,
138
+ releaseModuleSpendHold: () => releaseModuleSpendHold,
124
139
  reserveRollingPurchaseCaps: () => reserveRollingPurchaseCaps,
125
140
  selectSourceLots: () => selectSourceLots,
126
141
  serializeGbpMinorUnits: () => serializeGbpMinorUnits,
127
142
  serializeTokenSubunits: () => serializeTokenSubunits,
143
+ settleModuleSpendHold: () => settleModuleSpendHold,
128
144
  sortStringRecord: () => sortStringRecord,
129
145
  subunitsToWholeTokens: () => subunitsToWholeTokens,
130
146
  sumTokenSubunits: () => sumTokenSubunits,
@@ -1682,6 +1698,682 @@ function selectSourceLots(lots, requestedAmount, context) {
1682
1698
  );
1683
1699
  }
1684
1700
 
1701
+ // src/module-allowances.ts
1702
+ var MODULE_ALLOWANCE_PURPOSE = "junior-coder-module-entitlement";
1703
+ function assertWholePositive2(amount, label) {
1704
+ const parsed = parseTokenSubunits(amount);
1705
+ economyAssert(parsed > 0n, "INVALID_AMOUNT", `${label} must be positive`);
1706
+ economyAssert(
1707
+ isWholeTokenAmount(parsed),
1708
+ "AMOUNT_NOT_WHOLE_TOKEN",
1709
+ `${label} must contain whole Tokens`
1710
+ );
1711
+ return parsed;
1712
+ }
1713
+ function assertPositiveSlices(slices, expectedAmount, label) {
1714
+ economyAssert(slices.length > 0, "INVALID_CONTRACT", `${label} are required`);
1715
+ const lotIds = /* @__PURE__ */ new Set();
1716
+ for (const slice of slices) {
1717
+ assertEconomyIdentifier(slice.lotId, "lotId");
1718
+ economyAssert(
1719
+ !lotIds.has(slice.lotId),
1720
+ "DUPLICATE_IDENTIFIER",
1721
+ `${label} must not repeat a lot`
1722
+ );
1723
+ lotIds.add(slice.lotId);
1724
+ economyAssert(
1725
+ parseTokenSubunits(slice.amount) > 0n,
1726
+ "INVALID_AMOUNT",
1727
+ `${label} amounts must be positive`
1728
+ );
1729
+ }
1730
+ economyAssert(
1731
+ sumTokenSubunits(slices.map((slice) => slice.amount)) === expectedAmount,
1732
+ "INVALID_CONTRACT",
1733
+ `${label} must sum to the operation amount`
1734
+ );
1735
+ }
1736
+ function sumSlicesByLot(slices) {
1737
+ const totals = /* @__PURE__ */ new Map();
1738
+ for (const slice of slices) {
1739
+ assertEconomyIdentifier(slice.lotId, "lotId");
1740
+ const amount = parseTokenSubunits(slice.amount);
1741
+ economyAssert(amount > 0n, "INVALID_AMOUNT", "Lot slices must be positive");
1742
+ totals.set(slice.lotId, (totals.get(slice.lotId) ?? 0n) + amount);
1743
+ }
1744
+ return totals;
1745
+ }
1746
+ function assertSha256Reference(value, label) {
1747
+ economyAssert(
1748
+ typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value),
1749
+ "INVALID_CONTRACT",
1750
+ `${label} must be a canonical SHA-256 reference`
1751
+ );
1752
+ }
1753
+ function assertUpdatedAt(current, occurredAt, label) {
1754
+ economyAssert(
1755
+ parseIsoTimestamp(occurredAt) >= parseIsoTimestamp(current),
1756
+ "INVALID_TIME_WINDOW",
1757
+ `${label} cannot precede the current state`
1758
+ );
1759
+ }
1760
+ function assertModuleAllowance(allowance) {
1761
+ economyAssert(
1762
+ allowance.schemaVersion === ECONOMY_CONTRACT_VERSION && allowance.purpose === MODULE_ALLOWANCE_PURPOSE,
1763
+ "INVALID_CONTRACT",
1764
+ "Unsupported Module Allowance contract"
1765
+ );
1766
+ assertEconomyIdentifier(allowance.allowanceId, "allowanceId");
1767
+ assertEconomyIdentifier(allowance.householdId, "householdId");
1768
+ assertEconomyIdentifier(allowance.hostWalletId, "hostWalletId");
1769
+ assertEconomyIdentifier(allowance.allowanceWalletId, "allowanceWalletId");
1770
+ assertEconomyIdentifier(allowance.childAccountId, "childAccountId");
1771
+ economyAssert(
1772
+ allowance.hostWalletId !== allowance.allowanceWalletId,
1773
+ "INVALID_CONTRACT",
1774
+ "Host and Module Allowance wallets must be distinct"
1775
+ );
1776
+ economyAssert(
1777
+ allowance.status === "active" || allowance.status === "closed",
1778
+ "INVALID_CONTRACT",
1779
+ "Module Allowance status is unsupported"
1780
+ );
1781
+ const createdAt = parseIsoTimestamp(allowance.createdAt);
1782
+ const updatedAt = parseIsoTimestamp(allowance.updatedAt);
1783
+ economyAssert(
1784
+ updatedAt >= createdAt,
1785
+ "INVALID_TIME_WINDOW",
1786
+ "Module Allowance update cannot precede creation"
1787
+ );
1788
+ economyAssert(
1789
+ Number.isSafeInteger(allowance.version) && allowance.version >= 1,
1790
+ "INVALID_CONTRACT",
1791
+ "Module Allowance version must be a positive safe integer"
1792
+ );
1793
+ const allocated = parseTokenSubunits(allowance.allocatedAmount);
1794
+ const available = parseTokenSubunits(allowance.availableAmount);
1795
+ const held = parseTokenSubunits(allowance.heldAmount);
1796
+ const spent = parseTokenSubunits(allowance.spentAmount);
1797
+ const reclaimed = parseTokenSubunits(allowance.reclaimedAmount);
1798
+ economyAssert(
1799
+ allocated > 0n && [allocated, available, held, spent, reclaimed].every(
1800
+ (amount) => amount >= 0n && isWholeTokenAmount(amount)
1801
+ ),
1802
+ "AMOUNT_NOT_WHOLE_TOKEN",
1803
+ "Module Allowance amounts must be non-negative whole Tokens"
1804
+ );
1805
+ economyAssert(
1806
+ allocated === available + held + spent + reclaimed,
1807
+ "INVALID_CONTRACT",
1808
+ "Module Allowance buckets must equal its lifetime allocation"
1809
+ );
1810
+ economyAssert(
1811
+ allowance.status !== "closed" || available === 0n && held === 0n,
1812
+ "INVALID_CONTRACT",
1813
+ "A closed Module Allowance cannot retain available or held value"
1814
+ );
1815
+ const fundedByLot = sumSlicesByLot(allowance.fundingSlices);
1816
+ const reclaimedByLot = sumSlicesByLot(allowance.reclaimedSlices);
1817
+ economyAssert(
1818
+ sumTokenSubunits(allowance.fundingSlices.map((slice) => slice.amount)) === allocated,
1819
+ "INVALID_CONTRACT",
1820
+ "Module Allowance funding provenance must equal allocated value"
1821
+ );
1822
+ economyAssert(
1823
+ sumTokenSubunits(allowance.reclaimedSlices.map((slice) => slice.amount)) === reclaimed,
1824
+ "INVALID_CONTRACT",
1825
+ "Module Allowance reclaim provenance must equal reclaimed value"
1826
+ );
1827
+ for (const [lotId, amount] of reclaimedByLot) {
1828
+ economyAssert(
1829
+ amount <= (fundedByLot.get(lotId) ?? 0n),
1830
+ "SOURCE_LOT_RESTRICTED",
1831
+ "Reclaimed provenance cannot exceed Module Allowance funding"
1832
+ );
1833
+ }
1834
+ }
1835
+ function createModuleAllowance(input) {
1836
+ const amount = assertWholePositive2(input.amount, "Module Allowance amount");
1837
+ assertPositiveSlices(input.fundingSlices, amount, "Funding slices");
1838
+ const allowance = {
1839
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1840
+ purpose: MODULE_ALLOWANCE_PURPOSE,
1841
+ allowanceId: input.allowanceId,
1842
+ householdId: input.householdId,
1843
+ hostWalletId: input.hostWalletId,
1844
+ allowanceWalletId: input.allowanceWalletId,
1845
+ childAccountId: input.childAccountId,
1846
+ status: "active",
1847
+ allocatedAmount: input.amount,
1848
+ availableAmount: input.amount,
1849
+ heldAmount: serializeTokenSubunits(0n),
1850
+ spentAmount: serializeTokenSubunits(0n),
1851
+ reclaimedAmount: serializeTokenSubunits(0n),
1852
+ fundingSlices: input.fundingSlices,
1853
+ reclaimedSlices: [],
1854
+ version: 1,
1855
+ createdAt: input.occurredAt,
1856
+ updatedAt: input.occurredAt
1857
+ };
1858
+ assertModuleAllowance(allowance);
1859
+ return allowance;
1860
+ }
1861
+ function fundModuleAllowance(allowance, input) {
1862
+ assertModuleAllowance(allowance);
1863
+ economyAssert(
1864
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1865
+ "INVALID_CONTRACT",
1866
+ "Module Allowance is closed or its optimistic version is stale"
1867
+ );
1868
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Funding time");
1869
+ const amount = assertWholePositive2(input.amount, "Module Allowance funding");
1870
+ assertPositiveSlices(input.fundingSlices, amount, "Funding slices");
1871
+ const next = {
1872
+ ...allowance,
1873
+ allocatedAmount: serializeTokenSubunits(
1874
+ parseTokenSubunits(allowance.allocatedAmount) + amount
1875
+ ),
1876
+ availableAmount: serializeTokenSubunits(
1877
+ parseTokenSubunits(allowance.availableAmount) + amount
1878
+ ),
1879
+ fundingSlices: [...allowance.fundingSlices, ...input.fundingSlices],
1880
+ version: allowance.version + 1,
1881
+ updatedAt: input.occurredAt
1882
+ };
1883
+ assertModuleAllowance(next);
1884
+ return next;
1885
+ }
1886
+ function reclaimModuleAllowance(allowance, input) {
1887
+ assertModuleAllowance(allowance);
1888
+ economyAssert(
1889
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1890
+ "INVALID_CONTRACT",
1891
+ "Module Allowance is closed or its optimistic version is stale"
1892
+ );
1893
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Reclaim time");
1894
+ const amount = assertWholePositive2(input.amount, "Module Allowance reclaim");
1895
+ economyAssert(
1896
+ amount <= parseTokenSubunits(allowance.availableAmount),
1897
+ "INSUFFICIENT_BALANCE",
1898
+ "Cannot reclaim held, spent, or unavailable Module Allowance value"
1899
+ );
1900
+ assertPositiveSlices(input.sourceSlices, amount, "Reclaim slices");
1901
+ const fundedLotIds = new Set(
1902
+ allowance.fundingSlices.map((slice) => slice.lotId)
1903
+ );
1904
+ economyAssert(
1905
+ input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
1906
+ "SOURCE_LOT_RESTRICTED",
1907
+ "Reclaim slices must originate from the Module Allowance"
1908
+ );
1909
+ const next = {
1910
+ ...allowance,
1911
+ availableAmount: serializeTokenSubunits(
1912
+ parseTokenSubunits(allowance.availableAmount) - amount
1913
+ ),
1914
+ reclaimedAmount: serializeTokenSubunits(
1915
+ parseTokenSubunits(allowance.reclaimedAmount) + amount
1916
+ ),
1917
+ reclaimedSlices: [...allowance.reclaimedSlices, ...input.sourceSlices],
1918
+ version: allowance.version + 1,
1919
+ updatedAt: input.occurredAt
1920
+ };
1921
+ assertModuleAllowance(next);
1922
+ return next;
1923
+ }
1924
+ function closeModuleAllowance(allowance, input) {
1925
+ assertModuleAllowance(allowance);
1926
+ economyAssert(
1927
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1928
+ "INVALID_CONTRACT",
1929
+ "Module Allowance is closed or its optimistic version is stale"
1930
+ );
1931
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Closure time");
1932
+ economyAssert(
1933
+ parseTokenSubunits(allowance.availableAmount) === 0n && parseTokenSubunits(allowance.heldAmount) === 0n,
1934
+ "INVALID_CONTRACT",
1935
+ "Available value must be reclaimed and holds resolved before closure"
1936
+ );
1937
+ const next = {
1938
+ ...allowance,
1939
+ status: "closed",
1940
+ version: allowance.version + 1,
1941
+ updatedAt: input.occurredAt
1942
+ };
1943
+ assertModuleAllowance(next);
1944
+ return next;
1945
+ }
1946
+ function assertModuleSpendQuote(quote) {
1947
+ economyAssert(
1948
+ quote.schemaVersion === ECONOMY_CONTRACT_VERSION,
1949
+ "INVALID_CONTRACT",
1950
+ "Unsupported module-spend quote contract version"
1951
+ );
1952
+ assertEconomyIdentifier(quote.quoteId, "quoteId");
1953
+ assertEconomyIdentifier(quote.allowanceId, "allowanceId");
1954
+ assertEconomyIdentifier(quote.householdId, "householdId");
1955
+ assertEconomyIdentifier(quote.guardianAccountId, "guardianAccountId");
1956
+ assertEconomyIdentifier(quote.childAccountId, "childAccountId");
1957
+ assertEconomyIdentifier(quote.moduleVersionId, "moduleVersionId");
1958
+ assertEconomyIdentifier(quote.catalogVersion, "catalogVersion");
1959
+ assertEconomyIdentifier(
1960
+ quote.requirementsManifestVersion,
1961
+ "requirementsManifestVersion"
1962
+ );
1963
+ assertSha256Reference(
1964
+ quote.requirementsManifestHash,
1965
+ "Requirements manifest hash"
1966
+ );
1967
+ assertWholePositive2(quote.amount, "Module price");
1968
+ const acknowledgedAt = parseIsoTimestamp(quote.requirementsAcknowledgedAt);
1969
+ const issuedAt = parseIsoTimestamp(quote.issuedAt);
1970
+ const expiresAt = parseIsoTimestamp(quote.expiresAt);
1971
+ economyAssert(
1972
+ acknowledgedAt <= issuedAt && expiresAt > issuedAt,
1973
+ "INVALID_TIME_WINDOW",
1974
+ "Requirements must be acknowledged before a bounded quote is issued"
1975
+ );
1976
+ }
1977
+ function createModuleSpendQuote(input) {
1978
+ const quote = {
1979
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1980
+ ...input
1981
+ };
1982
+ assertModuleSpendQuote(quote);
1983
+ return quote;
1984
+ }
1985
+ function assertModuleSpendHold(hold) {
1986
+ economyAssert(
1987
+ hold.schemaVersion === ECONOMY_CONTRACT_VERSION,
1988
+ "INVALID_CONTRACT",
1989
+ "Unsupported module-spend hold contract version"
1990
+ );
1991
+ assertEconomyIdentifier(hold.holdId, "holdId");
1992
+ assertEconomyIdentifier(hold.quoteId, "quoteId");
1993
+ assertEconomyIdentifier(hold.allowanceId, "allowanceId");
1994
+ assertEconomyIdentifier(hold.householdId, "householdId");
1995
+ assertEconomyIdentifier(hold.guardianAccountId, "guardianAccountId");
1996
+ assertEconomyIdentifier(hold.childAccountId, "childAccountId");
1997
+ assertEconomyIdentifier(hold.moduleVersionId, "moduleVersionId");
1998
+ assertEconomyIdentifier(
1999
+ hold.requirementsManifestVersion,
2000
+ "requirementsManifestVersion"
2001
+ );
2002
+ assertSha256Reference(
2003
+ hold.requirementsManifestHash,
2004
+ "Requirements manifest hash"
2005
+ );
2006
+ assertEconomyIdentifier(hold.holdIdempotencyKey, "holdIdempotencyKey");
2007
+ const amount = assertWholePositive2(hold.amount, "Module hold amount");
2008
+ assertPositiveSlices(hold.sourceSlices, amount, "Hold source slices");
2009
+ economyAssert(
2010
+ ["held", "settled", "released"].includes(hold.status),
2011
+ "INVALID_CONTRACT",
2012
+ "Module hold status is unsupported"
2013
+ );
2014
+ economyAssert(
2015
+ Number.isSafeInteger(hold.version) && hold.version >= 1,
2016
+ "INVALID_CONTRACT",
2017
+ "Module hold version must be a positive safe integer"
2018
+ );
2019
+ const createdAt = parseIsoTimestamp(hold.createdAt);
2020
+ const expiresAt = parseIsoTimestamp(hold.expiresAt);
2021
+ const updatedAt = parseIsoTimestamp(hold.updatedAt);
2022
+ economyAssert(
2023
+ expiresAt > createdAt && updatedAt >= createdAt,
2024
+ "INVALID_TIME_WINDOW",
2025
+ "Module hold requires a bounded lifetime and ordered updates"
2026
+ );
2027
+ if (hold.status === "held") {
2028
+ economyAssert(
2029
+ hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.releaseTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseIdempotencyKey === void 0,
2030
+ "INVALID_CONTRACT",
2031
+ "An unsettled hold cannot claim an entitlement or final transaction"
2032
+ );
2033
+ } else if (hold.status === "settled") {
2034
+ economyAssert(
2035
+ hold.entitlementId !== void 0 && hold.settlementTransactionId !== void 0 && hold.settlementIdempotencyKey !== void 0 && hold.releaseTransactionId === void 0 && hold.releaseIdempotencyKey === void 0,
2036
+ "INVALID_CONTRACT",
2037
+ "A settled hold requires entitlement and transaction evidence"
2038
+ );
2039
+ assertEconomyIdentifier(hold.entitlementId, "entitlementId");
2040
+ assertEconomyIdentifier(
2041
+ hold.settlementTransactionId,
2042
+ "settlementTransactionId"
2043
+ );
2044
+ assertEconomyIdentifier(
2045
+ hold.settlementIdempotencyKey,
2046
+ "settlementIdempotencyKey"
2047
+ );
2048
+ } else {
2049
+ economyAssert(
2050
+ hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseTransactionId !== void 0 && hold.releaseIdempotencyKey !== void 0,
2051
+ "INVALID_CONTRACT",
2052
+ "A released hold requires release evidence only"
2053
+ );
2054
+ assertEconomyIdentifier(hold.releaseTransactionId, "releaseTransactionId");
2055
+ assertEconomyIdentifier(hold.releaseIdempotencyKey, "releaseIdempotencyKey");
2056
+ }
2057
+ }
2058
+ function assertAllowanceMatchesQuote(allowance, quote) {
2059
+ economyAssert(
2060
+ allowance.status === "active" && allowance.allowanceId === quote.allowanceId && allowance.householdId === quote.householdId && allowance.childAccountId === quote.childAccountId,
2061
+ "INVALID_CONTRACT",
2062
+ "Module quote does not belong to this active child allowance"
2063
+ );
2064
+ }
2065
+ function assertAllowanceMatchesHold(allowance, hold) {
2066
+ economyAssert(
2067
+ allowance.status === "active" && allowance.allowanceId === hold.allowanceId && allowance.householdId === hold.householdId && allowance.childAccountId === hold.childAccountId,
2068
+ "INVALID_CONTRACT",
2069
+ "Module hold does not belong to this active child allowance"
2070
+ );
2071
+ }
2072
+ function createModuleSpendHold(allowance, quote, input) {
2073
+ assertModuleAllowance(allowance);
2074
+ assertModuleSpendQuote(quote);
2075
+ assertAllowanceMatchesQuote(allowance, quote);
2076
+ economyAssert(
2077
+ allowance.version === input.expectedAllowanceVersion,
2078
+ "INVALID_CONTRACT",
2079
+ "Module Allowance optimistic version is stale"
2080
+ );
2081
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
2082
+ economyAssert(
2083
+ occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(quote.issuedAt) && occurredAt < parseIsoTimestamp(quote.expiresAt),
2084
+ "INVALID_TIME_WINDOW",
2085
+ "Module quote is not valid at hold time"
2086
+ );
2087
+ const amount = parseTokenSubunits(quote.amount);
2088
+ economyAssert(
2089
+ amount <= parseTokenSubunits(allowance.availableAmount),
2090
+ "INSUFFICIENT_BALANCE",
2091
+ "Module Allowance has insufficient available value"
2092
+ );
2093
+ assertEconomyIdentifier(input.holdId, "holdId");
2094
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2095
+ assertPositiveSlices(input.sourceSlices, amount, "Hold source slices");
2096
+ const fundedLotIds = new Set(
2097
+ allowance.fundingSlices.map((slice) => slice.lotId)
2098
+ );
2099
+ economyAssert(
2100
+ input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
2101
+ "SOURCE_LOT_RESTRICTED",
2102
+ "Hold slices must originate from the Module Allowance"
2103
+ );
2104
+ const nextAllowance = {
2105
+ ...allowance,
2106
+ availableAmount: serializeTokenSubunits(
2107
+ parseTokenSubunits(allowance.availableAmount) - amount
2108
+ ),
2109
+ heldAmount: serializeTokenSubunits(
2110
+ parseTokenSubunits(allowance.heldAmount) + amount
2111
+ ),
2112
+ version: allowance.version + 1,
2113
+ updatedAt: input.occurredAt
2114
+ };
2115
+ const hold = {
2116
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2117
+ holdId: input.holdId,
2118
+ quoteId: quote.quoteId,
2119
+ allowanceId: quote.allowanceId,
2120
+ householdId: quote.householdId,
2121
+ guardianAccountId: quote.guardianAccountId,
2122
+ childAccountId: quote.childAccountId,
2123
+ moduleVersionId: quote.moduleVersionId,
2124
+ amount: quote.amount,
2125
+ requirementsManifestVersion: quote.requirementsManifestVersion,
2126
+ requirementsManifestHash: quote.requirementsManifestHash,
2127
+ sourceSlices: input.sourceSlices,
2128
+ status: "held",
2129
+ holdIdempotencyKey: input.idempotencyKey,
2130
+ version: 1,
2131
+ createdAt: input.occurredAt,
2132
+ expiresAt: quote.expiresAt,
2133
+ updatedAt: input.occurredAt
2134
+ };
2135
+ assertModuleAllowance(nextAllowance);
2136
+ assertModuleSpendHold(hold);
2137
+ return { allowance: nextAllowance, hold };
2138
+ }
2139
+ function settleModuleSpendHold(allowance, hold, input) {
2140
+ assertModuleAllowance(allowance);
2141
+ assertModuleSpendHold(hold);
2142
+ assertAllowanceMatchesHold(allowance, hold);
2143
+ economyAssert(
2144
+ hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
2145
+ "INVALID_CONTRACT",
2146
+ "Module hold is final or an optimistic version is stale"
2147
+ );
2148
+ assertEconomyIdentifier(input.entitlementId, "entitlementId");
2149
+ assertEconomyIdentifier(
2150
+ input.settlementTransactionId,
2151
+ "settlementTransactionId"
2152
+ );
2153
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2154
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
2155
+ economyAssert(
2156
+ occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(hold.updatedAt) && occurredAt < parseIsoTimestamp(hold.expiresAt),
2157
+ "INVALID_TIME_WINDOW",
2158
+ "Module hold cannot settle outside its valid lifetime"
2159
+ );
2160
+ const amount = parseTokenSubunits(hold.amount);
2161
+ economyAssert(
2162
+ amount <= parseTokenSubunits(allowance.heldAmount),
2163
+ "INSUFFICIENT_BALANCE",
2164
+ "Module Allowance does not contain the quoted held value"
2165
+ );
2166
+ const nextAllowance = {
2167
+ ...allowance,
2168
+ heldAmount: serializeTokenSubunits(
2169
+ parseTokenSubunits(allowance.heldAmount) - amount
2170
+ ),
2171
+ spentAmount: serializeTokenSubunits(
2172
+ parseTokenSubunits(allowance.spentAmount) + amount
2173
+ ),
2174
+ version: allowance.version + 1,
2175
+ updatedAt: input.occurredAt
2176
+ };
2177
+ const nextHold = {
2178
+ ...hold,
2179
+ status: "settled",
2180
+ settlementIdempotencyKey: input.idempotencyKey,
2181
+ entitlementId: input.entitlementId,
2182
+ settlementTransactionId: input.settlementTransactionId,
2183
+ version: hold.version + 1,
2184
+ updatedAt: input.occurredAt
2185
+ };
2186
+ assertModuleAllowance(nextAllowance);
2187
+ assertModuleSpendHold(nextHold);
2188
+ return { allowance: nextAllowance, hold: nextHold };
2189
+ }
2190
+ function releaseModuleSpendHold(allowance, hold, input) {
2191
+ assertModuleAllowance(allowance);
2192
+ assertModuleSpendHold(hold);
2193
+ assertAllowanceMatchesHold(allowance, hold);
2194
+ economyAssert(
2195
+ hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
2196
+ "INVALID_CONTRACT",
2197
+ "Module hold is final or an optimistic version is stale"
2198
+ );
2199
+ assertEconomyIdentifier(input.releaseTransactionId, "releaseTransactionId");
2200
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2201
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
2202
+ economyAssert(
2203
+ occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(hold.updatedAt),
2204
+ "INVALID_TIME_WINDOW",
2205
+ "Module hold release cannot precede its current state"
2206
+ );
2207
+ const amount = parseTokenSubunits(hold.amount);
2208
+ economyAssert(
2209
+ amount <= parseTokenSubunits(allowance.heldAmount),
2210
+ "INSUFFICIENT_BALANCE",
2211
+ "Module Allowance does not contain the quoted held value"
2212
+ );
2213
+ const nextAllowance = {
2214
+ ...allowance,
2215
+ availableAmount: serializeTokenSubunits(
2216
+ parseTokenSubunits(allowance.availableAmount) + amount
2217
+ ),
2218
+ heldAmount: serializeTokenSubunits(
2219
+ parseTokenSubunits(allowance.heldAmount) - amount
2220
+ ),
2221
+ version: allowance.version + 1,
2222
+ updatedAt: input.occurredAt
2223
+ };
2224
+ const nextHold = {
2225
+ ...hold,
2226
+ status: "released",
2227
+ releaseIdempotencyKey: input.idempotencyKey,
2228
+ releaseTransactionId: input.releaseTransactionId,
2229
+ version: hold.version + 1,
2230
+ updatedAt: input.occurredAt
2231
+ };
2232
+ assertModuleAllowance(nextAllowance);
2233
+ assertModuleSpendHold(nextHold);
2234
+ return { allowance: nextAllowance, hold: nextHold };
2235
+ }
2236
+ function assertModulePurchaseReceipt(receipt) {
2237
+ economyAssert(
2238
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
2239
+ "INVALID_CONTRACT",
2240
+ "Unsupported module-purchase receipt contract version"
2241
+ );
2242
+ assertEconomyIdentifier(receipt.receiptId, "receiptId");
2243
+ assertEconomyIdentifier(receipt.quoteId, "quoteId");
2244
+ assertEconomyIdentifier(receipt.holdId, "holdId");
2245
+ assertEconomyIdentifier(receipt.allowanceId, "allowanceId");
2246
+ assertEconomyIdentifier(receipt.householdId, "householdId");
2247
+ assertEconomyIdentifier(receipt.guardianAccountId, "guardianAccountId");
2248
+ assertEconomyIdentifier(receipt.childAccountId, "childAccountId");
2249
+ assertEconomyIdentifier(receipt.moduleVersionId, "moduleVersionId");
2250
+ assertEconomyIdentifier(receipt.entitlementId, "entitlementId");
2251
+ assertEconomyIdentifier(
2252
+ receipt.requirementsManifestVersion,
2253
+ "requirementsManifestVersion"
2254
+ );
2255
+ assertSha256Reference(
2256
+ receipt.requirementsManifestHash,
2257
+ "Requirements manifest hash"
2258
+ );
2259
+ assertEconomyIdentifier(
2260
+ receipt.settlementTransactionId,
2261
+ "settlementTransactionId"
2262
+ );
2263
+ assertWholePositive2(receipt.amount, "Receipt amount");
2264
+ parseIsoTimestamp(receipt.issuedAt);
2265
+ }
2266
+ function createModulePurchaseReceipt(quote, hold, input) {
2267
+ assertModuleSpendQuote(quote);
2268
+ assertModuleSpendHold(hold);
2269
+ economyAssert(
2270
+ 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,
2271
+ "INVALID_CONTRACT",
2272
+ "Receipt quote and settled hold evidence do not match"
2273
+ );
2274
+ economyAssert(
2275
+ parseIsoTimestamp(input.issuedAt) >= parseIsoTimestamp(hold.updatedAt),
2276
+ "INVALID_TIME_WINDOW",
2277
+ "Receipt cannot precede settlement"
2278
+ );
2279
+ const receipt = {
2280
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2281
+ receiptId: input.receiptId,
2282
+ quoteId: quote.quoteId,
2283
+ holdId: hold.holdId,
2284
+ allowanceId: quote.allowanceId,
2285
+ householdId: quote.householdId,
2286
+ guardianAccountId: quote.guardianAccountId,
2287
+ childAccountId: quote.childAccountId,
2288
+ moduleVersionId: quote.moduleVersionId,
2289
+ entitlementId: hold.entitlementId,
2290
+ amount: quote.amount,
2291
+ requirementsManifestVersion: quote.requirementsManifestVersion,
2292
+ requirementsManifestHash: quote.requirementsManifestHash,
2293
+ settlementTransactionId: hold.settlementTransactionId,
2294
+ issuedAt: input.issuedAt
2295
+ };
2296
+ assertModulePurchaseReceipt(receipt);
2297
+ return receipt;
2298
+ }
2299
+ function assertModulePurchaseReconciliationObservation(observation) {
2300
+ economyAssert(
2301
+ observation.schemaVersion === ECONOMY_CONTRACT_VERSION,
2302
+ "INVALID_CONTRACT",
2303
+ "Unsupported module-purchase reconciliation contract version"
2304
+ );
2305
+ assertEconomyIdentifier(observation.quoteId, "quoteId");
2306
+ assertEconomyIdentifier(observation.holdId, "holdId");
2307
+ assertEconomyIdentifier(observation.childAccountId, "childAccountId");
2308
+ assertEconomyIdentifier(observation.moduleVersionId, "moduleVersionId");
2309
+ economyAssert(
2310
+ ["missing", "held", "settled", "released"].includes(
2311
+ observation.financialState
2312
+ ),
2313
+ "INVALID_CONTRACT",
2314
+ "Reconciliation financial state is unsupported"
2315
+ );
2316
+ economyAssert(
2317
+ ["missing", "pending", "active", "cancelled"].includes(
2318
+ observation.entitlementState
2319
+ ),
2320
+ "INVALID_CONTRACT",
2321
+ "Reconciliation entitlement state is unsupported"
2322
+ );
2323
+ economyAssert(
2324
+ typeof observation.receiptPresent === "boolean",
2325
+ "INVALID_CONTRACT",
2326
+ "Receipt presence must be Boolean"
2327
+ );
2328
+ parseIsoTimestamp(observation.observedAt);
2329
+ }
2330
+ function reconciliationResult(action, reasonCode, blocking = false, consistent = false) {
2331
+ return {
2332
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2333
+ action,
2334
+ blocking,
2335
+ consistent,
2336
+ reasonCode
2337
+ };
2338
+ }
2339
+ function reconcileModulePurchase(observation) {
2340
+ assertModulePurchaseReconciliationObservation(observation);
2341
+ const { financialState, entitlementState, receiptPresent } = observation;
2342
+ if (receiptPresent && !(financialState === "settled" && entitlementState === "active")) {
2343
+ return reconciliationResult(
2344
+ "manual-review",
2345
+ "receipt-conflicts-with-authoritative-state",
2346
+ true
2347
+ );
2348
+ }
2349
+ if (financialState === "settled" && entitlementState === "active") {
2350
+ return receiptPresent ? reconciliationResult("none", "purchase-consistent", false, true) : reconciliationResult("issue-receipt", "settled-active-receipt-missing");
2351
+ }
2352
+ if (financialState === "held" && entitlementState === "pending") {
2353
+ return reconciliationResult("resume-settlement", "held-pending");
2354
+ }
2355
+ if (financialState === "held" && (entitlementState === "missing" || entitlementState === "cancelled")) {
2356
+ return reconciliationResult("release-hold", "held-without-usable-entitlement");
2357
+ }
2358
+ if (financialState === "settled" && entitlementState === "pending") {
2359
+ return reconciliationResult("activate-entitlement", "settled-pending");
2360
+ }
2361
+ if ((financialState === "missing" || financialState === "released") && entitlementState === "pending") {
2362
+ return reconciliationResult(
2363
+ "cancel-pending-entitlement",
2364
+ "pending-without-settlement-path"
2365
+ );
2366
+ }
2367
+ if ((financialState === "missing" || financialState === "released") && (entitlementState === "missing" || entitlementState === "cancelled")) {
2368
+ return reconciliationResult("none", "no-active-purchase", false, true);
2369
+ }
2370
+ return reconciliationResult(
2371
+ "manual-review",
2372
+ financialState === "settled" ? "settled-debit-without-entitlement" : "active-entitlement-without-settled-debit",
2373
+ true
2374
+ );
2375
+ }
2376
+
1685
2377
  // src/operations.ts
1686
2378
  function assertDistinctAdjustmentApproval(request) {
1687
2379
  economyAssert(
@@ -4235,6 +4927,7 @@ function assertSpendRequest(request) {
4235
4927
  EARLY_BACKER_SETTLEMENT_POLICY_V2,
4236
4928
  ECONOMY_CONTRACT_VERSION,
4237
4929
  EconomyError,
4930
+ MODULE_ALLOWANCE_PURPOSE,
4238
4931
  PRE_UTILITY_BACKER_COHORT,
4239
4932
  TOKEN_SUBUNITS_MAX,
4240
4933
  TOKEN_SUBUNITS_MIN,
@@ -4259,6 +4952,11 @@ function assertSpendRequest(request) {
4259
4952
  assertFxSnapshot,
4260
4953
  assertGameplayAllocation,
4261
4954
  assertJournalChainHead,
4955
+ assertModuleAllowance,
4956
+ assertModulePurchaseReceipt,
4957
+ assertModulePurchaseReconciliationObservation,
4958
+ assertModuleSpendHold,
4959
+ assertModuleSpendQuote,
4262
4960
  assertNonNegativeAccounts,
4263
4961
  assertOpenPurchaseIntent,
4264
4962
  assertPaidLotLifecycle,
@@ -4302,10 +5000,15 @@ function assertSpendRequest(request) {
4302
5000
  calculateRollingPurchaseCapUsage,
4303
5001
  canUseSourceLot,
4304
5002
  canonicalTransactionPayload,
5003
+ closeModuleAllowance,
4305
5004
  convertRewardPayout,
4306
5005
  createEarlyBackerRetentionFromPaidLot,
4307
5006
  createGameplayAllocation,
4308
5007
  createInitialSourceLotSnapshot,
5008
+ createModuleAllowance,
5009
+ createModulePurchaseReceipt,
5010
+ createModuleSpendHold,
5011
+ createModuleSpendQuote,
4309
5012
  createPaidLotLifecycle,
4310
5013
  createProjectionSnapshot,
4311
5014
  createPurchaseIntentLifecycle,
@@ -4319,6 +5022,7 @@ function assertSpendRequest(request) {
4319
5022
  economyAssert,
4320
5023
  evaluateEarlyBacker,
4321
5024
  evaluateEarlyBackerBySettlementV2,
5025
+ fundModuleAllowance,
4322
5026
  isWholeTokenAmount,
4323
5027
  mapBitLabsConversionState,
4324
5028
  parseGbpMinorUnits,
@@ -4326,12 +5030,16 @@ function assertSpendRequest(request) {
4326
5030
  parseTokenSubunits,
4327
5031
  rebuildBalanceProjection,
4328
5032
  reclaimGameplayAllocation,
5033
+ reclaimModuleAllowance,
5034
+ reconcileModulePurchase,
4329
5035
  reducePaidLotLifecycleEvents,
4330
5036
  reducePurchaseIntentTransitions,
5037
+ releaseModuleSpendHold,
4331
5038
  reserveRollingPurchaseCaps,
4332
5039
  selectSourceLots,
4333
5040
  serializeGbpMinorUnits,
4334
5041
  serializeTokenSubunits,
5042
+ settleModuleSpendHold,
4335
5043
  sortStringRecord,
4336
5044
  subunitsToWholeTokens,
4337
5045
  sumTokenSubunits,