@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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/contracts/foundry.toml +9 -0
- package/contracts/src/ColdVault.sol +206 -0
- package/contracts/src/DepositFactory.sol +202 -0
- package/contracts/src/DepositProxy.sol +72 -0
- package/contracts/src/IERC20.sol +7 -0
- package/contracts/src/MockTokens.sol +32 -0
- package/contracts/src/SafeTransferLib.sol +31 -0
- package/contracts/test/Custody.t.sol +361 -0
- package/contracts/test/Vectors.t.sol +45 -0
- package/dist/audit.d.ts +265 -0
- package/dist/audit.js +291 -0
- package/dist/backup.d.ts +243 -0
- package/dist/backup.js +302 -0
- package/dist/calendar.d.ts +136 -0
- package/dist/calendar.js +129 -0
- package/dist/compliance.d.ts +172 -0
- package/dist/compliance.js +168 -0
- package/dist/finance/binance.d.ts +27 -0
- package/dist/finance/binance.js +452 -0
- package/dist/finance/chain-addresses.d.ts +130 -0
- package/dist/finance/chain-addresses.js +462 -0
- package/dist/finance/chain-deposits.d.ts +193 -0
- package/dist/finance/chain-deposits.js +596 -0
- package/dist/finance/chain-reconcile.d.ts +112 -0
- package/dist/finance/chain-reconcile.js +76 -0
- package/dist/finance/chain-withdrawals.d.ts +223 -0
- package/dist/finance/chain-withdrawals.js +631 -0
- package/dist/finance/chain.d.ts +116 -0
- package/dist/finance/chain.js +316 -0
- package/dist/finance/commission.d.ts +155 -0
- package/dist/finance/commission.js +419 -0
- package/dist/finance/custody.d.ts +68 -0
- package/dist/finance/custody.js +107 -0
- package/dist/finance/derive.d.ts +115 -0
- package/dist/finance/derive.js +116 -0
- package/dist/finance/discounts.d.ts +98 -0
- package/dist/finance/discounts.js +90 -0
- package/dist/finance/ledger.d.ts +221 -0
- package/dist/finance/ledger.js +308 -0
- package/dist/finance/market.d.ts +209 -0
- package/dist/finance/market.js +112 -0
- package/dist/finance/money.d.ts +118 -0
- package/dist/finance/money.js +176 -0
- package/dist/finance/rates.d.ts +178 -0
- package/dist/finance/rates.js +292 -0
- package/dist/finance/storage.d.ts +113 -0
- package/dist/finance/storage.js +226 -0
- package/dist/finance/tax.d.ts +132 -0
- package/dist/finance/tax.js +291 -0
- package/dist/finance/transfers.d.ts +153 -0
- package/dist/finance/transfers.js +292 -0
- package/dist/finance/venues.d.ts +190 -0
- package/dist/finance/venues.js +251 -0
- package/dist/identity.d.ts +115 -0
- package/dist/identity.js +111 -0
- package/dist/importers.d.ts +87 -0
- package/dist/importers.js +250 -0
- package/dist/jobs.d.ts +171 -0
- package/dist/jobs.js +250 -0
- package/dist/notify-templates.d.ts +11 -0
- package/dist/notify-templates.js +254 -0
- package/dist/notify.d.ts +172 -0
- package/dist/notify.js +122 -0
- package/dist/openssh.d.ts +36 -0
- package/dist/openssh.js +106 -0
- package/dist/otpauth.d.ts +57 -0
- package/dist/otpauth.js +223 -0
- package/dist/outbox.d.ts +234 -0
- package/dist/outbox.js +236 -0
- package/dist/passkey.d.ts +120 -0
- package/dist/passkey.js +105 -0
- package/dist/phrase.d.ts +87 -0
- package/dist/phrase.js +87 -0
- package/dist/pipeline.d.ts +137 -0
- package/dist/pipeline.js +121 -0
- package/dist/queue.d.ts +243 -0
- package/dist/queue.js +246 -0
- package/dist/schema-typebox.d.ts +24 -0
- package/dist/schema-typebox.js +201 -0
- package/dist/schema.d.ts +134 -0
- package/dist/schema.js +169 -0
- package/dist/serial.d.ts +54 -0
- package/dist/serial.js +40 -0
- package/dist/slip10.d.ts +37 -0
- package/dist/slip10.js +74 -0
- package/dist/snp.d.ts +115 -0
- package/dist/snp.js +109 -0
- package/dist/ssh-agent.d.ts +70 -0
- package/dist/ssh-agent.js +141 -0
- package/dist/ssh-cert.d.ts +73 -0
- package/dist/ssh-cert.js +111 -0
- package/dist/totp.d.ts +104 -0
- package/dist/totp.js +143 -0
- package/package.json +248 -0
|
@@ -0,0 +1,76 @@
|
|
|
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/chain-reconcile.ts
|
|
10
|
+
class ReconcileError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "ReconcileError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var DISCREPANCY_KINDS = ["shortfall", "surplus"];
|
|
19
|
+
var key = (item) => `${item.chain}:${item.asset}`;
|
|
20
|
+
function reconcileOnce(args) {
|
|
21
|
+
const chainSide = new Map(args.onChain.map((item) => [key(item), item]));
|
|
22
|
+
const ledgerSide = new Map(args.owed.map((item) => [key(item), item]));
|
|
23
|
+
const pairs = [...new Set([...chainSide.keys(), ...ledgerSide.keys()])].sort();
|
|
24
|
+
const discrepancies = [];
|
|
25
|
+
for (const pair of pairs) {
|
|
26
|
+
const held = chainSide.get(pair);
|
|
27
|
+
const liability = ledgerSide.get(pair);
|
|
28
|
+
const [chain, asset] = pair.split(":");
|
|
29
|
+
const onChainUnits = held?.units ?? 0n;
|
|
30
|
+
const owedUnits = (liability?.units ?? 0n) + (liability?.inFlightUnits ?? 0n);
|
|
31
|
+
const difference = onChainUnits - owedUnits;
|
|
32
|
+
const allowed = args.tolerance?.[asset] ?? 0n;
|
|
33
|
+
if (difference >= -allowed && difference <= allowed)
|
|
34
|
+
continue;
|
|
35
|
+
const kind = difference < 0n ? "shortfall" : "surplus";
|
|
36
|
+
discrepancies.push({
|
|
37
|
+
chain,
|
|
38
|
+
asset,
|
|
39
|
+
kind,
|
|
40
|
+
difference: { units: difference < 0n ? -difference : difference, asset },
|
|
41
|
+
onChain: { units: onChainUnits, asset },
|
|
42
|
+
owed: { units: owedUnits, asset },
|
|
43
|
+
meaning: kind === "shortfall" ? "The chain holds less than is owed. Withdrawals will fail once the balance is drawn down — " + "this is the state that becomes a customer-facing incident." : "Money arrived that nothing credited. Somebody is owed it and does not know, and attribution " + "gets harder the longer it sits."
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
atMs: args.atMs,
|
|
48
|
+
checked: pairs.length,
|
|
49
|
+
discrepancies,
|
|
50
|
+
balanced: discrepancies.length === 0
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function reconcileReport(report) {
|
|
54
|
+
if (report.balanced) {
|
|
55
|
+
return `Reconciled: ${report.checked} chain/asset pair(s) agree.`;
|
|
56
|
+
}
|
|
57
|
+
const shortfalls = report.discrepancies.filter((item) => item.kind === "shortfall");
|
|
58
|
+
const surpluses = report.discrepancies.filter((item) => item.kind === "surplus");
|
|
59
|
+
const line = (item) => ` ${item.kind === "shortfall" ? "SHORT" : "over "} ${item.chain}/${item.asset}: ` + `${item.difference.units} units (chain ${item.onChain.units}, owed ${item.owed.units})`;
|
|
60
|
+
return [
|
|
61
|
+
shortfalls.length > 0 ? `SHORTFALL on ${shortfalls.length} pair(s) — withdrawals will fail once drawn down.` : `No shortfall. ${surpluses.length} pair(s) hold more than is owed.`,
|
|
62
|
+
...shortfalls.map(line),
|
|
63
|
+
...surpluses.map(line)
|
|
64
|
+
].join(`
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
function shortfallUsd(report, usdValueOf) {
|
|
68
|
+
return report.discrepancies.filter((item) => item.kind === "shortfall").reduce((total, item) => total + usdValueOf(item.difference), 0);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
shortfallUsd,
|
|
72
|
+
reconcileReport,
|
|
73
|
+
reconcileOnce,
|
|
74
|
+
ReconcileError,
|
|
75
|
+
DISCREPANCY_KINDS
|
|
76
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { type Transaction } from './ledger';
|
|
2
|
+
import { type Money } from './money';
|
|
3
|
+
import { type ChainSpec } from './chain';
|
|
4
|
+
/**
|
|
5
|
+
* Getting money out, which is the most dangerous path in the product.
|
|
6
|
+
*
|
|
7
|
+
* Everything else here can be wrong and cost a correction. This one can be
|
|
8
|
+
* wrong and cost the money. Four things go wrong in practice and all four are
|
|
9
|
+
* arranged against explicitly:
|
|
10
|
+
*
|
|
11
|
+
* THE HOLD placed as part of accepting the request, never after. A gap
|
|
12
|
+
* between "accepted" and "held" is a window in which the same
|
|
13
|
+
* balance funds two withdrawals.
|
|
14
|
+
* THE BROADCAST keyed, so a retry after a timeout cannot send twice. A
|
|
15
|
+
* dispatcher that cannot tell "did not send" from "sent, reply
|
|
16
|
+
* lost" will eventually send twice.
|
|
17
|
+
* THE REFUND a transaction that fails AFTER the balance was debited leaves
|
|
18
|
+
* the user short. The prior art reconciled failed and rejected
|
|
19
|
+
* refunds in two separate passes, because both were found
|
|
20
|
+
* missing in production — separately.
|
|
21
|
+
* THE SWEEP on demand to fund a payout, never on a timer. A timer burns
|
|
22
|
+
* gas continuously moving dust nobody asked to move.
|
|
23
|
+
*
|
|
24
|
+
* ## A hold is not a lock
|
|
25
|
+
*
|
|
26
|
+
* It is a posting between the owner's `available` and `held` sub-accounts. So
|
|
27
|
+
* `availableOf()` is an ordinary balance query, there is no reservation table,
|
|
28
|
+
* no expiry sweeper, and a process that dies mid-withdrawal strands nothing —
|
|
29
|
+
* the posting either happened or it did not.
|
|
30
|
+
*/
|
|
31
|
+
export declare class WithdrawalError extends Error {
|
|
32
|
+
readonly code: 'BAD_STATE' | 'BAD_ADDRESS' | 'BAD_AMOUNT' | 'INSUFFICIENT' | 'CHAIN_DISABLED' | 'MALFORMED';
|
|
33
|
+
constructor(code: 'BAD_STATE' | 'BAD_ADDRESS' | 'BAD_AMOUNT' | 'INSUFFICIENT' | 'CHAIN_DISABLED' | 'MALFORMED', message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every state a withdrawal can be in, and there are no others.
|
|
37
|
+
*
|
|
38
|
+
* Written down as a list because the transitions are checked against it. An
|
|
39
|
+
* undeclared state reachable by assignment is how a withdrawal ends up
|
|
40
|
+
* `approved` twice, or dispatched from a state that never held the funds.
|
|
41
|
+
*/
|
|
42
|
+
export declare const WITHDRAWAL_STATES: readonly ["requested", "approved", "dispatched", "confirmed", "rejected", "failed", "refunded"];
|
|
43
|
+
export type WithdrawalState = (typeof WITHDRAWAL_STATES)[number];
|
|
44
|
+
export interface Withdrawal {
|
|
45
|
+
key: string;
|
|
46
|
+
owner: string;
|
|
47
|
+
chain: string;
|
|
48
|
+
/** Where the money is going. Validated against the chain's scheme. */
|
|
49
|
+
destination: string;
|
|
50
|
+
amount: Money;
|
|
51
|
+
state: WithdrawalState;
|
|
52
|
+
requestedAtMs: number;
|
|
53
|
+
/** The broadcast idempotency key. Present from `approved` onward. */
|
|
54
|
+
broadcastKey?: string;
|
|
55
|
+
txHash?: string;
|
|
56
|
+
decidedByUserKey?: string;
|
|
57
|
+
reason?: string;
|
|
58
|
+
}
|
|
59
|
+
/** May this withdrawal move there? */
|
|
60
|
+
export declare const canTransition: (from: WithdrawalState, to: WithdrawalState) => boolean;
|
|
61
|
+
/** The reference for the hold, and therefore the queue's dedupe key. */
|
|
62
|
+
export declare const holdReference: (key: string) => string;
|
|
63
|
+
/**
|
|
64
|
+
* The key a broadcast is made under.
|
|
65
|
+
*
|
|
66
|
+
* Derived from the withdrawal, so the same withdrawal produces the same key on
|
|
67
|
+
* every attempt. A dispatcher that timed out and retries presents this again;
|
|
68
|
+
* anything downstream that has seen it already knows this is the same send
|
|
69
|
+
* rather than a second one. Deriving it rather than generating it is the whole
|
|
70
|
+
* point — a generated key is a new key on every retry, which is exactly the
|
|
71
|
+
* failure it was meant to prevent.
|
|
72
|
+
*/
|
|
73
|
+
export declare const broadcastKeyFor: (withdrawal: Pick<Withdrawal, "key" | "chain">) => string;
|
|
74
|
+
/**
|
|
75
|
+
* Accept a request, and hold the funds in the same breath.
|
|
76
|
+
*
|
|
77
|
+
* Returns the withdrawal AND the posting together, so a caller cannot persist
|
|
78
|
+
* one without the other. Two functions — "create the request" then "place the
|
|
79
|
+
* hold" — would leave a window in which the balance is still available and a
|
|
80
|
+
* second request can spend it. They are one operation because they must not be
|
|
81
|
+
* separable.
|
|
82
|
+
*/
|
|
83
|
+
export declare function requestWithdrawal(args: {
|
|
84
|
+
key: string;
|
|
85
|
+
owner: string;
|
|
86
|
+
chain: ChainSpec;
|
|
87
|
+
destination: string;
|
|
88
|
+
amount: Money;
|
|
89
|
+
/** From `availableOf()` — what the owner can actually spend right now. */
|
|
90
|
+
available: Money;
|
|
91
|
+
atMs: number;
|
|
92
|
+
}): {
|
|
93
|
+
withdrawal: Withdrawal;
|
|
94
|
+
hold: Transaction;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* A human says yes.
|
|
98
|
+
*
|
|
99
|
+
* No posting: the funds were held at request time and stay held. Approval mints
|
|
100
|
+
* the broadcast key, which is the moment the send acquires an identity — before
|
|
101
|
+
* this there is nothing to be idempotent about.
|
|
102
|
+
*/
|
|
103
|
+
export declare function approveWithdrawal(withdrawal: Withdrawal, args: {
|
|
104
|
+
actorUserKey: string;
|
|
105
|
+
atMs: number;
|
|
106
|
+
}): Withdrawal;
|
|
107
|
+
/**
|
|
108
|
+
* A human says no, and the hold comes back.
|
|
109
|
+
*
|
|
110
|
+
* Reachable from `approved` as well as `requested`: a withdrawal approved and
|
|
111
|
+
* not yet broadcast can still be stopped, and that is exactly when somebody
|
|
112
|
+
* notices something is wrong.
|
|
113
|
+
*/
|
|
114
|
+
export declare function rejectWithdrawal(withdrawal: Withdrawal, args: {
|
|
115
|
+
actorUserKey: string;
|
|
116
|
+
reason: string;
|
|
117
|
+
atMs: number;
|
|
118
|
+
}): {
|
|
119
|
+
withdrawal: Withdrawal;
|
|
120
|
+
release: Transaction;
|
|
121
|
+
};
|
|
122
|
+
/** What a broadcaster must provide. Injected — runtime holds no credentials. */
|
|
123
|
+
export interface Broadcaster {
|
|
124
|
+
/**
|
|
125
|
+
* Send, or report that this key was already sent.
|
|
126
|
+
*
|
|
127
|
+
* The KEY is the argument that matters. An implementation that ignores it
|
|
128
|
+
* and sends anyway defeats the only protection against a double payout, so
|
|
129
|
+
* it is passed first and named for what it does.
|
|
130
|
+
*/
|
|
131
|
+
send(args: {
|
|
132
|
+
broadcastKey: string;
|
|
133
|
+
chain: string;
|
|
134
|
+
destination: string;
|
|
135
|
+
amount: Money;
|
|
136
|
+
}): Promise<{
|
|
137
|
+
txHash: string;
|
|
138
|
+
alreadySent: boolean;
|
|
139
|
+
}>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Broadcast one approved withdrawal.
|
|
143
|
+
*
|
|
144
|
+
* `alreadySent` is treated as success, not as an error. A dispatcher that timed
|
|
145
|
+
* out and retried is the ordinary case, and treating the second attempt as a
|
|
146
|
+
* failure would move a withdrawal that IS on-chain into `failed` and then
|
|
147
|
+
* refund it — paying the same money twice, which is the exact outcome the key
|
|
148
|
+
* exists to prevent.
|
|
149
|
+
*/
|
|
150
|
+
export declare function processOnce(withdrawal: Withdrawal, args: {
|
|
151
|
+
broadcaster: Broadcaster;
|
|
152
|
+
atMs: number;
|
|
153
|
+
}): Promise<Withdrawal>;
|
|
154
|
+
/**
|
|
155
|
+
* Seen on-chain, deep enough. The hold becomes a real debit.
|
|
156
|
+
*
|
|
157
|
+
* `captureHold` is what actually removes the money: until now it was held, not
|
|
158
|
+
* spent, and a withdrawal abandoned before this point costs the owner nothing.
|
|
159
|
+
*/
|
|
160
|
+
export declare function confirmWithdrawal(withdrawal: Withdrawal, args: {
|
|
161
|
+
txHash: string;
|
|
162
|
+
atMs: number;
|
|
163
|
+
}): {
|
|
164
|
+
withdrawal: Withdrawal;
|
|
165
|
+
capture: Transaction;
|
|
166
|
+
};
|
|
167
|
+
/** Broadcast and failed on-chain. Marked, not yet made good. */
|
|
168
|
+
export declare function markFailed(withdrawal: Withdrawal, args: {
|
|
169
|
+
reason: string;
|
|
170
|
+
atMs: number;
|
|
171
|
+
}): Withdrawal;
|
|
172
|
+
/**
|
|
173
|
+
* Make a failed withdrawal good.
|
|
174
|
+
*
|
|
175
|
+
* A separate pass from rejection on purpose. They look like one problem — money
|
|
176
|
+
* to return — and they are two: a rejection never left the platform, whereas a
|
|
177
|
+
* failure was broadcast and did not land. The prior art ran them as one and
|
|
178
|
+
* found BOTH refund paths missing in production, separately, which is the
|
|
179
|
+
* strongest argument available for keeping them apart.
|
|
180
|
+
*/
|
|
181
|
+
export declare function refundFailed(withdrawal: Withdrawal, args: {
|
|
182
|
+
atMs: number;
|
|
183
|
+
}): {
|
|
184
|
+
withdrawal: Withdrawal;
|
|
185
|
+
release: Transaction;
|
|
186
|
+
};
|
|
187
|
+
/** A deposit address holding funds that have not been moved to the hot wallet. */
|
|
188
|
+
export interface SweepCandidate {
|
|
189
|
+
address: string;
|
|
190
|
+
owner: string;
|
|
191
|
+
amount: Money;
|
|
192
|
+
/** What it costs to move it, in the same asset terms as `amount`. */
|
|
193
|
+
estimatedGas: Money;
|
|
194
|
+
}
|
|
195
|
+
export interface SweepPlan {
|
|
196
|
+
/** Addresses worth sweeping, largest first. */
|
|
197
|
+
sweep: readonly SweepCandidate[];
|
|
198
|
+
/** Skipped because moving them costs more than they hold. */
|
|
199
|
+
uneconomic: readonly SweepCandidate[];
|
|
200
|
+
/** What the plan raises, net of gas. */
|
|
201
|
+
raised: Money;
|
|
202
|
+
/** True when the plan cannot fund the payout even after sweeping everything. */
|
|
203
|
+
short: boolean;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Which deposit addresses to sweep to fund one payout.
|
|
207
|
+
*
|
|
208
|
+
* ON DEMAND, never on a timer. A timer sweeps continuously and burns gas moving
|
|
209
|
+
* dust nobody asked to move; funding a specific payout sweeps the least it can
|
|
210
|
+
* and stops. That is why this takes a target rather than a schedule.
|
|
211
|
+
*
|
|
212
|
+
* Largest first, so the fewest transactions raise the amount — each sweep is
|
|
213
|
+
* its own gas cost, and twenty small ones cost more than two large ones raising
|
|
214
|
+
* the same total.
|
|
215
|
+
*/
|
|
216
|
+
export declare function sweepForPayout(args: {
|
|
217
|
+
need: Money;
|
|
218
|
+
/** Already in the hot wallet and spendable without sweeping anything. */
|
|
219
|
+
hot: Money;
|
|
220
|
+
candidates: readonly SweepCandidate[];
|
|
221
|
+
}): SweepPlan;
|
|
222
|
+
/** Human-readable, for an operator screen. */
|
|
223
|
+
export declare const describeSweep: (plan: SweepPlan) => string;
|