@forgezero/runtime 0.1.13 → 0.1.15
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 +60 -1133
- package/dist/notify-templates.js +1 -1
- package/dist/schema-typebox.js +116 -7
- package/dist/schema.d.ts +14 -1
- package/dist/schema.js +116 -7
- package/package.json +2 -58
- package/dist/compliance.d.ts +0 -172
- package/dist/compliance.js +0 -168
- package/dist/finance/chain-addresses.d.ts +0 -130
- package/dist/finance/chain-addresses.js +0 -462
- package/dist/finance/chain-deposits.d.ts +0 -193
- package/dist/finance/chain-deposits.js +0 -600
- package/dist/finance/chain-reconcile.d.ts +0 -112
- package/dist/finance/chain-reconcile.js +0 -76
- package/dist/finance/chain-withdrawals.d.ts +0 -223
- package/dist/finance/chain-withdrawals.js +0 -635
- package/dist/finance/chain.d.ts +0 -116
- package/dist/finance/chain.js +0 -316
- package/dist/finance/commission.d.ts +0 -155
- package/dist/finance/commission.js +0 -423
- package/dist/finance/custody.d.ts +0 -68
- package/dist/finance/custody.js +0 -107
- package/dist/finance/derive.d.ts +0 -115
- package/dist/finance/derive.js +0 -116
- package/dist/finance/ledger.d.ts +0 -227
- package/dist/finance/ledger.js +0 -313
- package/dist/finance/market.d.ts +0 -209
- package/dist/finance/market.js +0 -112
- package/dist/finance/rates.d.ts +0 -178
- package/dist/finance/rates.js +0 -292
- package/dist/finance/transfers.d.ts +0 -153
- package/dist/finance/transfers.js +0 -292
- package/dist/finance/venues.d.ts +0 -190
- package/dist/finance/venues.js +0 -251
package/dist/compliance.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
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/compliance.ts
|
|
10
|
-
import { Refusal } from "@forgezero/access";
|
|
11
|
-
|
|
12
|
-
class ComplianceError extends Error {
|
|
13
|
-
code;
|
|
14
|
-
constructor(code, message) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.name = "ComplianceError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
var RISK_LEVELS = ["low", "medium", "high", "prohibited"];
|
|
21
|
-
var VERIFICATION_TIERS = ["none", "basic", "verified", "enhanced"];
|
|
22
|
-
var tierLimitRule = (limits) => ({
|
|
23
|
-
name: "tier.limit",
|
|
24
|
-
check(subject) {
|
|
25
|
-
const tier = subject.tier ?? "none";
|
|
26
|
-
const limit = limits[tier];
|
|
27
|
-
if (limit === undefined || subject.usdValue === undefined)
|
|
28
|
-
return;
|
|
29
|
-
if (subject.usdValue <= limit)
|
|
30
|
-
return;
|
|
31
|
-
return {
|
|
32
|
-
rule: "tier.limit",
|
|
33
|
-
risk: "high",
|
|
34
|
-
detail: `$${subject.usdValue} exceeds the $${limit} limit for a ${tier} account.`
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
var countryRule = (prohibited) => ({
|
|
39
|
-
name: "country.prohibited",
|
|
40
|
-
check(subject) {
|
|
41
|
-
const country = subject.attributes?.country?.toUpperCase();
|
|
42
|
-
if (!country || !prohibited.includes(country))
|
|
43
|
-
return;
|
|
44
|
-
return {
|
|
45
|
-
rule: "country.prohibited",
|
|
46
|
-
risk: "prohibited",
|
|
47
|
-
detail: `${country} is not served.`
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
var newCounterpartyRule = (args) => ({
|
|
52
|
-
name: "counterparty.new",
|
|
53
|
-
async check(subject) {
|
|
54
|
-
if (!subject.address || (subject.usdValue ?? 0) < args.aboveUsd)
|
|
55
|
-
return;
|
|
56
|
-
if (await args.isKnown(subject.address))
|
|
57
|
-
return;
|
|
58
|
-
return {
|
|
59
|
-
rule: "counterparty.new",
|
|
60
|
-
risk: "medium",
|
|
61
|
-
detail: `First transfer with ${subject.address.slice(0, 10)}… above $${args.aboveUsd}.`
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
var ORDER = { low: 0, medium: 1, high: 2, prohibited: 3 };
|
|
66
|
-
var highest = (levels) => levels.reduce((worst, level) => ORDER[level] > ORDER[worst] ? level : worst, "low");
|
|
67
|
-
async function screen(subject, options = {}) {
|
|
68
|
-
const now = options.now ?? Date.now;
|
|
69
|
-
if (!subject.owner) {
|
|
70
|
-
throw new ComplianceError("BAD_SUBJECT", "A subject needs an owner to screen.");
|
|
71
|
-
}
|
|
72
|
-
let listMatches = [];
|
|
73
|
-
let listVersion = "none";
|
|
74
|
-
if (options.list) {
|
|
75
|
-
listVersion = options.list.version;
|
|
76
|
-
listMatches = await options.list.match(subject);
|
|
77
|
-
}
|
|
78
|
-
const hits = [];
|
|
79
|
-
for (const rule of options.rules ?? []) {
|
|
80
|
-
const hit = await rule.check(subject);
|
|
81
|
-
if (hit)
|
|
82
|
-
hits.push(hit);
|
|
83
|
-
}
|
|
84
|
-
const risk = highest([
|
|
85
|
-
...listMatches.length > 0 ? ["prohibited"] : [],
|
|
86
|
-
...hits.map((hit) => hit.risk)
|
|
87
|
-
]);
|
|
88
|
-
const reviewAt = options.reviewAt ?? "high";
|
|
89
|
-
const decision = risk === "prohibited" ? "refuse" : ORDER[risk] >= ORDER[reviewAt] ? "review" : "allow";
|
|
90
|
-
return { decision, risk, hits, listMatches, listVersion, screenedAtMs: now(), subject };
|
|
91
|
-
}
|
|
92
|
-
function screeningStage(options = {}) {
|
|
93
|
-
return {
|
|
94
|
-
name: "aml.screening",
|
|
95
|
-
order: 0,
|
|
96
|
-
async run(transfer) {
|
|
97
|
-
const subject = {
|
|
98
|
-
owner: transfer.owner,
|
|
99
|
-
address: transfer.address,
|
|
100
|
-
network: transfer.network,
|
|
101
|
-
direction: transfer.direction,
|
|
102
|
-
usdValue: Number(transfer.context?.usdValue ?? 0),
|
|
103
|
-
tier: transfer.context?.tier ?? "none",
|
|
104
|
-
attributes: transfer.context?.attributes
|
|
105
|
-
};
|
|
106
|
-
let verdict;
|
|
107
|
-
try {
|
|
108
|
-
verdict = await screen(subject, options);
|
|
109
|
-
} catch (cause) {
|
|
110
|
-
if (cause instanceof ComplianceError && cause.code === "LIST_UNAVAILABLE") {
|
|
111
|
-
throw new Refusal(503, "SCREENING_UNAVAILABLE", "Screening could not be completed. The transfer has not been processed.", {}, true);
|
|
112
|
-
}
|
|
113
|
-
throw cause;
|
|
114
|
-
}
|
|
115
|
-
await options.record?.(verdict);
|
|
116
|
-
if (verdict.decision === "refuse") {
|
|
117
|
-
throw new Refusal(451, "SCREENING_REFUSED", "This transfer cannot be processed.", {
|
|
118
|
-
risk: verdict.risk,
|
|
119
|
-
rules: verdict.hits.map((hit) => hit.rule).join(",")
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (verdict.decision === "review" && !await options.isApproved?.(subject)) {
|
|
123
|
-
throw new Refusal(409, "SCREENING_REVIEW", "This transfer is held for review.", { risk: verdict.risk }, true);
|
|
124
|
-
}
|
|
125
|
-
return { screenedAtMs: verdict.screenedAtMs, risk: verdict.risk, listVersion: verdict.listVersion };
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function staticList(entries, version = "static-1") {
|
|
130
|
-
const byValue = new Map(entries.map((entry) => [entry.value.toLowerCase(), entry]));
|
|
131
|
-
return {
|
|
132
|
-
version,
|
|
133
|
-
async match(subject) {
|
|
134
|
-
const candidates = [subject.address, subject.attributes?.name, subject.attributes?.country].filter((value) => Boolean(value)).map((value) => value.toLowerCase());
|
|
135
|
-
return candidates.map((value) => byValue.get(value)).filter((entry) => entry !== undefined);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
function combineLists(...lists) {
|
|
140
|
-
return {
|
|
141
|
-
version: lists.map((list) => list.version).join("+"),
|
|
142
|
-
async match(subject) {
|
|
143
|
-
const all = [];
|
|
144
|
-
for (const list of lists)
|
|
145
|
-
all.push(...await list.match(subject));
|
|
146
|
-
return all;
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
var unavailableList = (reason) => ({
|
|
151
|
-
version: "unavailable",
|
|
152
|
-
async match() {
|
|
153
|
-
throw new ComplianceError("LIST_UNAVAILABLE", reason);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
export {
|
|
157
|
-
unavailableList,
|
|
158
|
-
tierLimitRule,
|
|
159
|
-
staticList,
|
|
160
|
-
screeningStage,
|
|
161
|
-
screen,
|
|
162
|
-
newCounterpartyRule,
|
|
163
|
-
countryRule,
|
|
164
|
-
combineLists,
|
|
165
|
-
VERIFICATION_TIERS,
|
|
166
|
-
RISK_LEVELS,
|
|
167
|
-
ComplianceError
|
|
168
|
-
};
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { depositAddress, toChecksumAddress } from './custody';
|
|
2
|
-
import type { AddressScheme, ChainSpec } from './chain';
|
|
3
|
-
/**
|
|
4
|
-
* One deposit address per user per chain, derived rather than allocated.
|
|
5
|
-
*
|
|
6
|
-
* The naive design gives each user a real account created on-chain. That costs
|
|
7
|
-
* gas per user before they have deposited anything, so onboarding a hundred
|
|
8
|
-
* thousand people costs real money for the ones who never fund. Worse, the
|
|
9
|
-
* address list becomes state: lose it and the deposits keep arriving somewhere
|
|
10
|
-
* nobody is watching.
|
|
11
|
-
*
|
|
12
|
-
* A counterfactual CREATE2 address costs nothing. The contract that would live
|
|
13
|
-
* there is not deployed until there is something to sweep out of it, and until
|
|
14
|
-
* then it is a pure function of (factory, salt, init code hash). The whole
|
|
15
|
-
* address book is therefore reproducible from a backup of three values, and
|
|
16
|
-
* "restore the deposit addresses" stops being a recovery procedure at all.
|
|
17
|
-
*
|
|
18
|
-
* ## What this adds over `custody.ts`
|
|
19
|
-
*
|
|
20
|
-
* Almost nothing, deliberately. `custody.depositAddress` already mirrors the
|
|
21
|
-
* deployed factory byte for byte, and `chain.validateAddress` already knows the
|
|
22
|
-
* shape of every scheme. What was missing is the CONVENTION on top: which salt
|
|
23
|
-
* a given user on a given chain gets, and the refusal to answer at all for a
|
|
24
|
-
* chain whose scheme has no counterfactual derivation.
|
|
25
|
-
*
|
|
26
|
-
* Re-exporting rather than reimplementing is the point. Two functions that both
|
|
27
|
-
* compute a deposit address are two chances to disagree, and the one that
|
|
28
|
-
* disagrees sends money somewhere unreachable.
|
|
29
|
-
*/
|
|
30
|
-
export { validateAddress, type AddressScheme } from './chain';
|
|
31
|
-
/**
|
|
32
|
-
* Which schemes can derive an address without touching the chain.
|
|
33
|
-
*
|
|
34
|
-
* Only EVM, today. Bitcoin's equivalent is an HD derivation path and Solana's
|
|
35
|
-
* is a program-derived address — both are real designs and neither is this one,
|
|
36
|
-
* so a chain using them must not silently fall through to a formula written for
|
|
37
|
-
* something else. Listing what IS supported rather than excluding what is not
|
|
38
|
-
* means adding a chain fails closed.
|
|
39
|
-
*/
|
|
40
|
-
export declare const COUNTERFACTUAL_SCHEMES: readonly AddressScheme[];
|
|
41
|
-
export declare class AddressError extends Error {
|
|
42
|
-
readonly code: 'SCHEME_UNSUPPORTED' | 'CHAIN_DISABLED' | 'MISSING_DEPLOYMENT' | 'MALFORMED';
|
|
43
|
-
constructor(code: 'SCHEME_UNSUPPORTED' | 'CHAIN_DISABLED' | 'MISSING_DEPLOYMENT' | 'MALFORMED', message: string);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Where a chain's factory lives, and what it deploys.
|
|
47
|
-
*
|
|
48
|
-
* `proxyInitCodeHash` is read from the DEPLOYED factory rather than compiled in.
|
|
49
|
-
* A Solidity version bump changes the init code and therefore every address
|
|
50
|
-
* derived from it; a hardcoded constant would keep deriving addresses the live
|
|
51
|
-
* factory cannot sweep, and the failure appears only once somebody has already
|
|
52
|
-
* sent funds to one.
|
|
53
|
-
*/
|
|
54
|
-
export interface ChainDeployment {
|
|
55
|
-
chain: string;
|
|
56
|
-
factory: string;
|
|
57
|
-
proxyInitCodeHash: string;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The salt for one owner on one chain.
|
|
61
|
-
*
|
|
62
|
-
* The CHAIN is inside the salt even though each chain has its own factory. Two
|
|
63
|
-
* chains that deploy the factory to the same address — which happens, because a
|
|
64
|
-
* deterministic deployer is the normal way to deploy one — would otherwise
|
|
65
|
-
* derive the same deposit address for the same user on both. That is not
|
|
66
|
-
* immediately a loss, but it makes the address ambiguous in every log, alert
|
|
67
|
-
* and support conversation afterwards, and it costs nothing to prevent here.
|
|
68
|
-
*/
|
|
69
|
-
export declare const depositSaltFor: (args: {
|
|
70
|
-
chain: string;
|
|
71
|
-
owner: string;
|
|
72
|
-
}) => string;
|
|
73
|
-
/**
|
|
74
|
-
* The deposit address for one owner on one chain.
|
|
75
|
-
*
|
|
76
|
-
* Pure: same inputs, same answer, forever. That property is what lets a scanner
|
|
77
|
-
* recompute the whole address book on every pass instead of trusting a stored
|
|
78
|
-
* list, and what makes a stored list a cache rather than a record.
|
|
79
|
-
*/
|
|
80
|
-
export declare function deriveDepositAddress(args: {
|
|
81
|
-
chain: ChainSpec;
|
|
82
|
-
deployment: ChainDeployment;
|
|
83
|
-
owner: string;
|
|
84
|
-
}): string;
|
|
85
|
-
/**
|
|
86
|
-
* The raw CREATE2 formula, exposed for anything deriving something else.
|
|
87
|
-
*
|
|
88
|
-
* A thin alias over `custody.depositAddress` so callers with their own salt
|
|
89
|
-
* convention — a per-invoice address, say — do not reach past this module and
|
|
90
|
-
* end up with a second copy of the formula.
|
|
91
|
-
*/
|
|
92
|
-
export declare const create2Address: typeof depositAddress;
|
|
93
|
-
/**
|
|
94
|
-
* Every address to watch on one chain, for a set of owners.
|
|
95
|
-
*
|
|
96
|
-
* The scanner needs the whole set on every pass, keyed for lookup rather than
|
|
97
|
-
* listed: matching a block's transfers against an array is a scan per transfer,
|
|
98
|
-
* and a busy block against a large user base is where that becomes the reason a
|
|
99
|
-
* deposit is late.
|
|
100
|
-
*
|
|
101
|
-
* Keyed LOWERCASE. Chains report addresses in whatever case they please, and a
|
|
102
|
-
* checksum-cased key silently fails to match a lowercase one — a deposit that
|
|
103
|
-
* arrives, is seen, and is not credited.
|
|
104
|
-
*/
|
|
105
|
-
export declare function depositAddressBook(args: {
|
|
106
|
-
chain: ChainSpec;
|
|
107
|
-
deployment: ChainDeployment;
|
|
108
|
-
owners: readonly string[];
|
|
109
|
-
}): Map<string, string>;
|
|
110
|
-
/**
|
|
111
|
-
* Whose address is this, if anyone's?
|
|
112
|
-
*
|
|
113
|
-
* Case-insensitive for the reason above. Returns `null` rather than throwing:
|
|
114
|
-
* most addresses in any block belong to nobody here, and that is the normal
|
|
115
|
-
* case rather than an error.
|
|
116
|
-
*/
|
|
117
|
-
export declare const ownerOfAddress: (book: Map<string, string>, address: string) => string | null;
|
|
118
|
-
/**
|
|
119
|
-
* Check a derived address against what the chain actually reports.
|
|
120
|
-
*
|
|
121
|
-
* Worth doing once per deployment, not per address: if these disagree the
|
|
122
|
-
* formula and the factory have diverged, and every address derived since is
|
|
123
|
-
* wrong. Comparing case-insensitively because the checksum casing is a display
|
|
124
|
-
* convention — a mismatch that is only casing is not a divergence.
|
|
125
|
-
*/
|
|
126
|
-
export declare function assertDerivationMatches(args: {
|
|
127
|
-
derived: string;
|
|
128
|
-
reportedByChain: string;
|
|
129
|
-
}): void;
|
|
130
|
-
export { toChecksumAddress };
|