@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/README.md +51 -5
- package/dist/index.cjs +708 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -1
- package/dist/index.d.ts +204 -1
- package/dist/index.js +692 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|