@forgezero/runtime 0.1.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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/contracts/foundry.toml +9 -0
  4. package/contracts/src/ColdVault.sol +206 -0
  5. package/contracts/src/DepositFactory.sol +202 -0
  6. package/contracts/src/DepositProxy.sol +72 -0
  7. package/contracts/src/IERC20.sol +7 -0
  8. package/contracts/src/MockTokens.sol +32 -0
  9. package/contracts/src/SafeTransferLib.sol +31 -0
  10. package/contracts/test/Custody.t.sol +361 -0
  11. package/contracts/test/Vectors.t.sol +45 -0
  12. package/dist/audit.d.ts +265 -0
  13. package/dist/audit.js +291 -0
  14. package/dist/backup.d.ts +243 -0
  15. package/dist/backup.js +302 -0
  16. package/dist/calendar.d.ts +136 -0
  17. package/dist/calendar.js +129 -0
  18. package/dist/compliance.d.ts +172 -0
  19. package/dist/compliance.js +168 -0
  20. package/dist/finance/binance.d.ts +27 -0
  21. package/dist/finance/binance.js +452 -0
  22. package/dist/finance/chain-addresses.d.ts +130 -0
  23. package/dist/finance/chain-addresses.js +462 -0
  24. package/dist/finance/chain-deposits.d.ts +193 -0
  25. package/dist/finance/chain-deposits.js +596 -0
  26. package/dist/finance/chain-reconcile.d.ts +112 -0
  27. package/dist/finance/chain-reconcile.js +76 -0
  28. package/dist/finance/chain-withdrawals.d.ts +223 -0
  29. package/dist/finance/chain-withdrawals.js +631 -0
  30. package/dist/finance/chain.d.ts +116 -0
  31. package/dist/finance/chain.js +316 -0
  32. package/dist/finance/commission.d.ts +155 -0
  33. package/dist/finance/commission.js +419 -0
  34. package/dist/finance/custody.d.ts +68 -0
  35. package/dist/finance/custody.js +107 -0
  36. package/dist/finance/derive.d.ts +115 -0
  37. package/dist/finance/derive.js +116 -0
  38. package/dist/finance/discounts.d.ts +98 -0
  39. package/dist/finance/discounts.js +90 -0
  40. package/dist/finance/ledger.d.ts +221 -0
  41. package/dist/finance/ledger.js +308 -0
  42. package/dist/finance/market.d.ts +209 -0
  43. package/dist/finance/market.js +112 -0
  44. package/dist/finance/money.d.ts +118 -0
  45. package/dist/finance/money.js +176 -0
  46. package/dist/finance/rates.d.ts +178 -0
  47. package/dist/finance/rates.js +292 -0
  48. package/dist/finance/storage.d.ts +113 -0
  49. package/dist/finance/storage.js +226 -0
  50. package/dist/finance/tax.d.ts +132 -0
  51. package/dist/finance/tax.js +291 -0
  52. package/dist/finance/transfers.d.ts +153 -0
  53. package/dist/finance/transfers.js +292 -0
  54. package/dist/finance/venues.d.ts +190 -0
  55. package/dist/finance/venues.js +251 -0
  56. package/dist/identity.d.ts +115 -0
  57. package/dist/identity.js +111 -0
  58. package/dist/importers.d.ts +87 -0
  59. package/dist/importers.js +250 -0
  60. package/dist/jobs.d.ts +171 -0
  61. package/dist/jobs.js +250 -0
  62. package/dist/notify-templates.d.ts +11 -0
  63. package/dist/notify-templates.js +254 -0
  64. package/dist/notify.d.ts +172 -0
  65. package/dist/notify.js +122 -0
  66. package/dist/openssh.d.ts +36 -0
  67. package/dist/openssh.js +106 -0
  68. package/dist/otpauth.d.ts +57 -0
  69. package/dist/otpauth.js +223 -0
  70. package/dist/outbox.d.ts +234 -0
  71. package/dist/outbox.js +236 -0
  72. package/dist/passkey.d.ts +120 -0
  73. package/dist/passkey.js +105 -0
  74. package/dist/phrase.d.ts +87 -0
  75. package/dist/phrase.js +87 -0
  76. package/dist/pipeline.d.ts +137 -0
  77. package/dist/pipeline.js +121 -0
  78. package/dist/queue.d.ts +243 -0
  79. package/dist/queue.js +246 -0
  80. package/dist/schema-typebox.d.ts +24 -0
  81. package/dist/schema-typebox.js +201 -0
  82. package/dist/schema.d.ts +134 -0
  83. package/dist/schema.js +169 -0
  84. package/dist/serial.d.ts +54 -0
  85. package/dist/serial.js +40 -0
  86. package/dist/slip10.d.ts +37 -0
  87. package/dist/slip10.js +74 -0
  88. package/dist/snp.d.ts +115 -0
  89. package/dist/snp.js +109 -0
  90. package/dist/ssh-agent.d.ts +70 -0
  91. package/dist/ssh-agent.js +141 -0
  92. package/dist/ssh-cert.d.ts +73 -0
  93. package/dist/ssh-cert.js +111 -0
  94. package/dist/totp.d.ts +104 -0
  95. package/dist/totp.js +143 -0
  96. package/package.json +248 -0
@@ -0,0 +1,132 @@
1
+ import { type Money, type Rounding } from './money';
2
+ /**
3
+ * What tax is due, where, and who accounts for it.
4
+ *
5
+ * Rates are DATA, never constants here. VAT changes by ministerial decree with
6
+ * weeks of notice, and a rate compiled into a package means a release to comply
7
+ * with a law that already applies. What this owns is the mechanism: which
8
+ * jurisdiction may tax a sale, whether the seller or the buyer accounts for it,
9
+ * and how the figure is rounded.
10
+ *
11
+ * ## The three ways to be zero, which are not the same
12
+ *
13
+ * zero-rated taxable at 0%. Appears on a return, and input tax is
14
+ * recoverable.
15
+ * exempt outside the tax's scope. Also 0% on the invoice, and input
16
+ * tax is NOT recoverable.
17
+ * reverse charge the buyer accounts for it, not the seller. 0% charged, and
18
+ * the invoice must SAY SO or it is not a valid invoice.
19
+ *
20
+ * Collapsing them into "tax: 0" produces correct-looking invoices and an
21
+ * unfilable return. They are distinct here for that reason alone.
22
+ *
23
+ * ## Place of supply
24
+ *
25
+ * For digital services sold to a consumer, tax is due where the CUSTOMER is,
26
+ * not where the seller is. Getting this backwards is the single most common
27
+ * cross-border invoicing error, and it under-collects in exactly the
28
+ * jurisdictions most likely to audit.
29
+ */
30
+ export declare class TaxError extends Error {
31
+ readonly code: 'UNKNOWN_JURISDICTION' | 'BAD_RATE' | 'NO_COUNTRY' | 'INCONSISTENT';
32
+ constructor(code: 'UNKNOWN_JURISDICTION' | 'BAD_RATE' | 'NO_COUNTRY' | 'INCONSISTENT', message: string);
33
+ }
34
+ export declare const TREATMENTS: readonly ["standard", "zero-rated", "exempt", "reverse-charge", "outside-scope"];
35
+ export type Treatment = (typeof TREATMENTS)[number];
36
+ export interface TaxRate {
37
+ /** ISO 3166-1 alpha-2, uppercase. */
38
+ country: string;
39
+ /** Sub-jurisdiction where one taxes separately — a US state, a CA province. */
40
+ region?: string;
41
+ /** `VAT`, `GST`, `sales`. Named because it must appear on the invoice. */
42
+ label: string;
43
+ /** Percent as a decimal string: `'0.20'` is 20%. Never a float. */
44
+ rate: string;
45
+ /** Businesses in this country may account for it themselves. */
46
+ reverseChargeAvailable?: boolean;
47
+ }
48
+ export interface Customer {
49
+ country: string;
50
+ region?: string;
51
+ /** A validated registration number. Its PRESENCE is what makes a sale B2B. */
52
+ taxId?: string;
53
+ }
54
+ export interface Supplier {
55
+ country: string;
56
+ region?: string;
57
+ taxId?: string;
58
+ }
59
+ export interface TaxDecision {
60
+ treatment: Treatment;
61
+ /** Where tax is due. Empty when nothing is. */
62
+ jurisdiction: string;
63
+ label: string;
64
+ /** The applied percent. `'0'` for every non-standard treatment. */
65
+ rate: string;
66
+ /** Wording the invoice must carry, when the law requires a statement. */
67
+ note?: string;
68
+ }
69
+ /**
70
+ * Which treatment applies to this sale.
71
+ *
72
+ * `digitalService` matters: for those, place of supply is the CUSTOMER's
73
+ * country. For anything else it is the supplier's, and the difference decides
74
+ * which country's rate is charged.
75
+ */
76
+ export declare function decideTax(args: {
77
+ supplier: Supplier;
78
+ customer: Customer;
79
+ rates: readonly TaxRate[];
80
+ digitalService?: boolean;
81
+ /** Countries in a common VAT area, where reverse charge applies between them. */
82
+ commonArea?: readonly string[];
83
+ }): TaxDecision;
84
+ export interface TaxAmounts {
85
+ net: Money;
86
+ tax: Money;
87
+ gross: Money;
88
+ }
89
+ /**
90
+ * Split an amount into net, tax and gross.
91
+ *
92
+ * `inclusive` says whether the figure already contains the tax. A price
93
+ * advertised to consumers usually does and a B2B price usually does not, and
94
+ * applying the wrong one is a percentage-sized error in the direction nobody
95
+ * notices until a return is filed.
96
+ *
97
+ * Rounding is explicit and defaults DOWN — toward under-charging rather than
98
+ * over-charging, because the second is the one that gets refunded with a
99
+ * complaint attached.
100
+ */
101
+ export declare function applyTax(amount: Money, decision: TaxDecision, options?: {
102
+ inclusive?: boolean;
103
+ rounding?: Rounding;
104
+ }): TaxAmounts;
105
+ /** A human-readable percent for an invoice line: `'0.20'` becomes `'20%'`. */
106
+ export declare const percent: (rate: string) => string;
107
+ /**
108
+ * Total several lines, taxing each and summing.
109
+ *
110
+ * Per-LINE rounding, then summed — not the reverse. Taxing the total instead
111
+ * produces a figure that does not equal the sum of the printed lines, and an
112
+ * invoice whose lines do not add up to its total is one a customer refuses to
113
+ * pay and an auditor refuses to accept.
114
+ */
115
+ export declare function totalWithTax(lines: readonly Money[], decision: TaxDecision, options?: {
116
+ inclusive?: boolean;
117
+ rounding?: Rounding;
118
+ }): TaxAmounts;
119
+ /** Formatted for a document, so a template never does arithmetic. */
120
+ export declare const describeTax: (amounts: TaxAmounts, decision: TaxDecision) => {
121
+ net: string;
122
+ tax: string;
123
+ gross: string;
124
+ label: string;
125
+ rate: string;
126
+ treatment: "standard" | "zero-rated" | "exempt" | "reverse-charge" | "outside-scope";
127
+ jurisdiction: string;
128
+ note: string | undefined;
129
+ };
130
+ /** A rate table entry, validated. */
131
+ export declare function assertTaxRate(rate: TaxRate): void;
132
+ export declare const parseRate: (value: string, asset: string) => Money;
@@ -0,0 +1,291 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/finance/money.ts
10
+ class MoneyError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = "MoneyError";
16
+ }
17
+ }
18
+ var ASSETS = [
19
+ { code: "USDT", decimals: 6 },
20
+ { code: "USDC", decimals: 6 },
21
+ { code: "BTC", decimals: 8 },
22
+ { code: "ETH", decimals: 18 },
23
+ { code: "BNB", decimals: 18 },
24
+ { code: "EUR", decimals: 2 },
25
+ { code: "USD", decimals: 2 }
26
+ ];
27
+ var REGISTRY = new Map(ASSETS.map((asset) => [asset.code, asset]));
28
+ function defineAsset(spec) {
29
+ if (spec.decimals < 0 || spec.decimals > 30 || !Number.isInteger(spec.decimals)) {
30
+ throw new MoneyError("UNKNOWN_ASSET", `${spec.code}: decimals must be an integer 0–30.`);
31
+ }
32
+ REGISTRY.set(spec.code, spec);
33
+ }
34
+ function assetSpec(code) {
35
+ const spec = REGISTRY.get(code);
36
+ if (!spec)
37
+ throw new MoneyError("UNKNOWN_ASSET", `Unknown asset "${code}". Call defineAsset first.`);
38
+ return spec;
39
+ }
40
+ var money = (units, asset) => {
41
+ assetSpec(asset);
42
+ return { units, asset };
43
+ };
44
+ var zero = (asset) => money(0n, asset);
45
+ function parseAmount(value, asset) {
46
+ const spec = assetSpec(asset);
47
+ const text = value.trim();
48
+ if (!/^-?\d+(\.\d+)?$/.test(text)) {
49
+ throw new MoneyError("NOT_FINITE", `"${value}" is not a plain decimal amount.`);
50
+ }
51
+ const negative = text.startsWith("-");
52
+ const [whole, fraction = ""] = text.replace("-", "").split(".");
53
+ if (fraction.length > spec.decimals) {
54
+ throw new MoneyError("PRECISION_LOSS", `${asset} has ${spec.decimals} decimals; "${value}" has ${fraction.length}.`);
55
+ }
56
+ const padded = fraction.padEnd(spec.decimals, "0");
57
+ const units = BigInt(whole + padded);
58
+ return { units: negative ? -units : units, asset };
59
+ }
60
+ function formatAmount(amount, options = {}) {
61
+ const spec = assetSpec(amount.asset);
62
+ const negative = amount.units < 0n;
63
+ const digits = (negative ? -amount.units : amount.units).toString().padStart(spec.decimals + 1, "0");
64
+ const whole = digits.slice(0, digits.length - spec.decimals);
65
+ let fraction = spec.decimals === 0 ? "" : digits.slice(digits.length - spec.decimals);
66
+ if (options.trim && fraction)
67
+ fraction = fraction.replace(/0+$/, "");
68
+ return `${negative ? "-" : ""}${whole}${fraction ? `.${fraction}` : ""}`;
69
+ }
70
+ function sameAsset(a, b) {
71
+ if (a.asset !== b.asset) {
72
+ throw new MoneyError("ASSET_MISMATCH", `Cannot combine ${a.asset} and ${b.asset}.`);
73
+ }
74
+ }
75
+ function add(a, b) {
76
+ sameAsset(a, b);
77
+ return { units: a.units + b.units, asset: a.asset };
78
+ }
79
+ function subtract(a, b) {
80
+ sameAsset(a, b);
81
+ return { units: a.units - b.units, asset: a.asset };
82
+ }
83
+ var negate = (amount) => ({ units: -amount.units, asset: amount.asset });
84
+ var abs = (amount) => ({
85
+ units: amount.units < 0n ? -amount.units : amount.units,
86
+ asset: amount.asset
87
+ });
88
+ var isZero = (amount) => amount.units === 0n;
89
+ var isNegative = (amount) => amount.units < 0n;
90
+ function compare(a, b) {
91
+ sameAsset(a, b);
92
+ return a.units < b.units ? -1 : a.units > b.units ? 1 : 0;
93
+ }
94
+ var equals = (a, b) => a.asset === b.asset && a.units === b.units;
95
+ var ROUNDING = ["down", "up", "half-up"];
96
+ function divideRounded(numerator, denominator, mode) {
97
+ if (denominator === 0n)
98
+ throw new MoneyError("DIVIDE_BY_ZERO", "Division by zero.");
99
+ const negative = numerator < 0n !== denominator < 0n;
100
+ const a = numerator < 0n ? -numerator : numerator;
101
+ const b = denominator < 0n ? -denominator : denominator;
102
+ const quotient = a / b;
103
+ const remainder = a % b;
104
+ if (remainder === 0n)
105
+ return negative ? -quotient : quotient;
106
+ let result = quotient;
107
+ if (mode === "up")
108
+ result += 1n;
109
+ else if (mode === "half-up" && remainder * 2n >= b)
110
+ result += 1n;
111
+ return negative ? -result : result;
112
+ }
113
+ function mulRate(amount, rate, mode = "down") {
114
+ if (!/^-?\d+(\.\d+)?$/.test(rate.trim())) {
115
+ throw new MoneyError("NOT_FINITE", `"${rate}" is not a plain decimal rate.`);
116
+ }
117
+ const [whole, fraction = ""] = rate.trim().replace("-", "").split(".");
118
+ const scale = 10n ** BigInt(fraction.length);
119
+ const scaled = BigInt(whole + fraction) * (rate.trim().startsWith("-") ? -1n : 1n);
120
+ return { units: divideRounded(amount.units * scaled, scale, mode), asset: amount.asset };
121
+ }
122
+ function convert(amount, to, rate, mode = "down") {
123
+ const from = assetSpec(amount.asset);
124
+ const target = assetSpec(to);
125
+ const asTarget = mulRate({ units: amount.units, asset: to }, rate, mode);
126
+ const shift = target.decimals - from.decimals;
127
+ if (shift === 0)
128
+ return asTarget;
129
+ if (shift > 0)
130
+ return { units: asTarget.units * 10n ** BigInt(shift), asset: to };
131
+ return { units: divideRounded(asTarget.units, 10n ** BigInt(-shift), mode), asset: to };
132
+ }
133
+ function allocate(amount, parts) {
134
+ if (parts < 1)
135
+ throw new MoneyError("NOT_FINITE", "Cannot allocate into fewer than one part.");
136
+ const each = divideRounded(amount.units, BigInt(parts), "down");
137
+ const allocated = Array.from({ length: parts }, () => each);
138
+ let remainder = amount.units - each * BigInt(parts);
139
+ const step = remainder < 0n ? -1n : 1n;
140
+ for (let index = 0;remainder !== 0n; index = (index + 1) % parts) {
141
+ allocated[index] += step;
142
+ remainder -= step;
143
+ }
144
+ return allocated.map((units) => ({ units, asset: amount.asset }));
145
+ }
146
+ function toStep(amount, step, mode = "down") {
147
+ const stepUnits = parseAmount(step, amount.asset).units;
148
+ if (stepUnits <= 0n)
149
+ throw new MoneyError("NOT_FINITE", "A step must be positive.");
150
+ return { units: divideRounded(amount.units, stepUnits, mode) * stepUnits, asset: amount.asset };
151
+ }
152
+ var VERSION = "0.1.0";
153
+
154
+ // src/finance/tax.ts
155
+ class TaxError extends Error {
156
+ code;
157
+ constructor(code, message) {
158
+ super(message);
159
+ this.code = code;
160
+ this.name = "TaxError";
161
+ }
162
+ }
163
+ var TREATMENTS = ["standard", "zero-rated", "exempt", "reverse-charge", "outside-scope"];
164
+ var normaliseCountry = (value, what) => {
165
+ if (!value || !/^[A-Za-z]{2}$/.test(value)) {
166
+ throw new TaxError("NO_COUNTRY", `${what} needs a two-letter country code.`);
167
+ }
168
+ return value.toUpperCase();
169
+ };
170
+ var findRate = (rates, country, region) => {
171
+ const regional = region ? rates.find((r) => r.country === country && r.region?.toUpperCase() === region.toUpperCase()) : undefined;
172
+ return regional ?? rates.find((r) => r.country === country && !r.region);
173
+ };
174
+ function decideTax(args) {
175
+ const supplierCountry = normaliseCountry(args.supplier.country, "The supplier");
176
+ const customerCountry = normaliseCountry(args.customer.country, "The customer");
177
+ const domestic = supplierCountry === customerCountry;
178
+ const area = (args.commonArea ?? []).map((c) => c.toUpperCase());
179
+ const bothInArea = area.includes(supplierCountry) && area.includes(customerCountry);
180
+ const place = args.digitalService && !domestic ? customerCountry : supplierCountry;
181
+ const region = place === customerCountry ? args.customer.region : args.supplier.region;
182
+ const rate = findRate(args.rates, place, region);
183
+ if (domestic) {
184
+ if (!rate) {
185
+ throw new TaxError("UNKNOWN_JURISDICTION", `No rate is configured for ${place}.`);
186
+ }
187
+ return {
188
+ treatment: "standard",
189
+ jurisdiction: rate.region ? `${place}-${rate.region}` : place,
190
+ label: rate.label,
191
+ rate: rate.rate
192
+ };
193
+ }
194
+ if (bothInArea && args.customer.taxId && rate?.reverseChargeAvailable !== false) {
195
+ return {
196
+ treatment: "reverse-charge",
197
+ jurisdiction: customerCountry,
198
+ label: rate?.label ?? "VAT",
199
+ rate: "0",
200
+ note: "Reverse charge: the customer accounts for the tax."
201
+ };
202
+ }
203
+ if (bothInArea) {
204
+ if (!rate) {
205
+ throw new TaxError("UNKNOWN_JURISDICTION", `No rate is configured for ${place}.`);
206
+ }
207
+ return {
208
+ treatment: "standard",
209
+ jurisdiction: place,
210
+ label: rate.label,
211
+ rate: rate.rate
212
+ };
213
+ }
214
+ return {
215
+ treatment: "outside-scope",
216
+ jurisdiction: "",
217
+ label: rate?.label ?? "",
218
+ rate: "0",
219
+ note: "Outside the scope of tax in the supplier country."
220
+ };
221
+ }
222
+ function applyTax(amount, decision, options = {}) {
223
+ const rate = Number(decision.rate);
224
+ if (!Number.isFinite(rate) || rate < 0 || rate > 1) {
225
+ throw new TaxError("BAD_RATE", `${decision.rate} is not a rate between 0 and 1.`);
226
+ }
227
+ if (rate === 0) {
228
+ return { net: amount, tax: zero(amount.asset), gross: amount };
229
+ }
230
+ const bp = BigInt(Math.round(rate * 1e4));
231
+ if (options.inclusive) {
232
+ const net = amount.units * 10000n / (10000n + bp);
233
+ const netMoney = { units: net, asset: amount.asset };
234
+ return { net: netMoney, tax: subtract(amount, netMoney), gross: amount };
235
+ }
236
+ const tax = { units: amount.units * bp / 10000n, asset: amount.asset };
237
+ return { net: amount, tax, gross: add(amount, tax) };
238
+ }
239
+ var percent = (rate) => {
240
+ const value = Number(rate) * 100;
241
+ return `${Number.isInteger(value) ? value : value.toFixed(2)}%`;
242
+ };
243
+ function totalWithTax(lines, decision, options = {}) {
244
+ if (lines.length === 0) {
245
+ throw new TaxError("INCONSISTENT", "An invoice needs at least one line.");
246
+ }
247
+ const asset = lines[0].asset;
248
+ let net = zero(asset);
249
+ let tax = zero(asset);
250
+ for (const line of lines) {
251
+ if (line.asset !== asset) {
252
+ throw new TaxError("INCONSISTENT", `Lines mix ${asset} and ${line.asset}.`);
253
+ }
254
+ const amounts = applyTax(line, decision, options);
255
+ net = add(net, amounts.net);
256
+ tax = add(tax, amounts.tax);
257
+ }
258
+ return { net, tax, gross: add(net, tax) };
259
+ }
260
+ var describeTax = (amounts, decision) => ({
261
+ net: formatAmount(amounts.net),
262
+ tax: formatAmount(amounts.tax),
263
+ gross: formatAmount(amounts.gross),
264
+ label: decision.label,
265
+ rate: percent(decision.rate),
266
+ treatment: decision.treatment,
267
+ jurisdiction: decision.jurisdiction,
268
+ note: decision.note
269
+ });
270
+ function assertTaxRate(rate) {
271
+ normaliseCountry(rate.country, "A rate");
272
+ const value = Number(rate.rate);
273
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
274
+ throw new TaxError("BAD_RATE", `${rate.country}: ${rate.rate} is not a rate between 0 and 1.`);
275
+ }
276
+ if (!rate.label) {
277
+ throw new TaxError("BAD_RATE", `${rate.country}: a rate needs a label for the invoice.`);
278
+ }
279
+ }
280
+ var parseRate = (value, asset) => parseAmount(value, asset);
281
+ export {
282
+ totalWithTax,
283
+ percent,
284
+ parseRate,
285
+ describeTax,
286
+ decideTax,
287
+ assertTaxRate,
288
+ applyTax,
289
+ TaxError,
290
+ TREATMENTS
291
+ };
@@ -0,0 +1,153 @@
1
+ import { Refusal } from '@forgezero/access';
2
+ import { type Money } from './money';
3
+ /**
4
+ * Deposits and withdrawals as ORDERED PIPELINES, not as functions.
5
+ *
6
+ * The seam exists before the thing that plugs into it, and that ordering is the
7
+ * whole point. AML screening is deferred, but the position it will occupy —
8
+ * first, before anything else looks at the transfer — is decided now. Adding a
9
+ * first-position check to a function later means editing every path money
10
+ * takes, and the one path somebody misses is the one that matters.
11
+ *
12
+ * So a transfer runs a list of stages. Adding screening later is registering a
13
+ * stage. Nothing in deposit or withdrawal code changes.
14
+ *
15
+ * ## Stages refuse the way routes refuse
16
+ *
17
+ * A stage throws `Refusal` from `@forgezero/access`, which already carries a
18
+ * status, a code, details and a retryable flag — and the HTTP pipeline already
19
+ * knows how to turn one into a response. So a screening hit and a route denial
20
+ * are the same shape, are logged the same way, and reach a client the same way.
21
+ * Inventing a second failure type here would mean translating between them at
22
+ * every boundary, and a translation layer is where a `retryable` flag gets lost.
23
+ *
24
+ * ## What this does NOT do
25
+ *
26
+ * No balance movement, no persistence, no ordering. Postings belong to
27
+ * `./ledger` and ordering belongs to the queue keyed by account owner. This
28
+ * decides only whether a transfer may proceed, and records why not.
29
+ */
30
+ export declare const DIRECTIONS: readonly ["deposit", "withdrawal"];
31
+ export type Direction = (typeof DIRECTIONS)[number];
32
+ export interface Transfer {
33
+ direction: Direction;
34
+ /** The account owner. Also the queue key the caller should be holding. */
35
+ owner: string;
36
+ amount: Money;
37
+ /** Chain, venue, or wherever this is coming from or going to. */
38
+ network?: string;
39
+ /** Destination for a withdrawal, source for a deposit. */
40
+ address?: string;
41
+ /** On-chain transaction hash, once one exists. */
42
+ txHash?: string;
43
+ /** Idempotency key. The queue's `dedupeKey` for this transfer. */
44
+ reference: string;
45
+ atMs: number;
46
+ /** Anything a stage wants to pass to a later one. */
47
+ context?: Record<string, unknown>;
48
+ }
49
+ /**
50
+ * One check.
51
+ *
52
+ * Returning normally means "proceed". Throwing `Refusal` stops the transfer
53
+ * with a reason a client can read. A stage may also return a patch, which is
54
+ * merged into `context` for later stages — that is how screening passes a risk
55
+ * score to an approval stage without either knowing about the other.
56
+ */
57
+ export interface Stage {
58
+ name: string;
59
+ /**
60
+ * Where this runs. Lower numbers first, ties broken by registration order.
61
+ *
62
+ * Explicit rather than array position, because the ordering IS the security
63
+ * property here and a list somebody reorders while tidying is a screening
64
+ * check that silently moved behind the balance check.
65
+ */
66
+ order: number;
67
+ directions?: readonly Direction[];
68
+ run(transfer: Transfer): Promise<void | Record<string, unknown>> | void | Record<string, unknown>;
69
+ }
70
+ /** Reserved for the check that must always be first. */
71
+ export declare const SCREENING_ORDER = 0;
72
+ export interface StageOutcome {
73
+ stage: string;
74
+ ok: boolean;
75
+ ms: number;
76
+ code?: string;
77
+ }
78
+ export interface PipelineResult {
79
+ ok: boolean;
80
+ transfer: Transfer;
81
+ stages: StageOutcome[];
82
+ /** The refusal that stopped it. Absent when it passed. */
83
+ refusal?: Refusal;
84
+ }
85
+ export declare class PipelineError extends Error {
86
+ readonly code: 'DUPLICATE_STAGE' | 'NO_SCREENING';
87
+ constructor(code: 'DUPLICATE_STAGE' | 'NO_SCREENING', message: string);
88
+ }
89
+ /**
90
+ * Build a pipeline from stages.
91
+ *
92
+ * `requireScreening` is off by default and ON for production wiring: a platform
93
+ * that has decided it needs AML should fail to start rather than run without
94
+ * it. A missing screening stage is exactly the kind of omission that is
95
+ * invisible until somebody audits, so it is expressible as a boot-time
96
+ * assertion rather than a runbook item.
97
+ */
98
+ export declare function createTransferPipeline(options: {
99
+ stages: readonly Stage[];
100
+ requireScreening?: boolean;
101
+ now?: () => number;
102
+ /** Called for every stage, passed or refused. The audit hook. */
103
+ onStage?: (outcome: StageOutcome, transfer: Transfer) => void;
104
+ }): {
105
+ stages: {
106
+ name: string;
107
+ order: number;
108
+ }[];
109
+ /**
110
+ * Run every stage that applies, in order, stopping at the first refusal.
111
+ *
112
+ * First refusal, not all of them: the stages are a sequence where each may
113
+ * depend on the last having passed, so continuing past one would run checks
114
+ * against a transfer that has already been rejected — and report failures
115
+ * that are consequences rather than causes.
116
+ */
117
+ run(transfer: Transfer): Promise<PipelineResult>;
118
+ };
119
+ export type TransferPipeline = ReturnType<typeof createTransferPipeline>;
120
+ /**
121
+ * The AML placeholder.
122
+ *
123
+ * Registered at order 0 and currently passes everything, which is what the
124
+ * owner asked for. Its value is entirely in existing: the position is agreed,
125
+ * the audit trail already records it running, and replacing the body later
126
+ * changes no caller.
127
+ *
128
+ * `allow` is a parameter rather than a hard-coded `true` so a test can prove
129
+ * the pipeline actually stops on a screening refusal — a stage that has never
130
+ * refused anything is a stage nobody knows works.
131
+ */
132
+ export declare const screeningStage: (check?: (transfer: Transfer) => Promise<boolean> | boolean) => Stage;
133
+ /** Amount bounds, per direction. */
134
+ export declare const limitsStage: (limits: {
135
+ min?: Money;
136
+ max?: Money;
137
+ directions?: readonly Direction[];
138
+ }) => Stage;
139
+ /**
140
+ * Enough available balance, for a withdrawal.
141
+ *
142
+ * Available, never total — a check against total lets a withdrawal spend what
143
+ * an open order has already committed. Runs AFTER screening, deliberately: a
144
+ * screened-out transfer should not reveal whether the balance was sufficient.
145
+ */
146
+ export declare const balanceStage: (available: (transfer: Transfer) => Promise<Money> | Money) => Stage;
147
+ /** Hold a withdrawal for a human above a threshold. */
148
+ export declare const approvalStage: (args: {
149
+ threshold: Money;
150
+ isApproved: (transfer: Transfer) => Promise<boolean> | boolean;
151
+ }) => Stage;
152
+ /** Refuse while an account is frozen for non-payment. */
153
+ export declare const notFrozenStage: (isFrozen: (owner: string) => Promise<boolean> | boolean) => Stage;