@ninsei-labs/sdk 0.39.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/CHANGELOG.md +866 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/engine/atomic/dleq2.js +330 -0
- package/engine/atomic/halfenc.js +205 -0
- package/engine/atomic/halves.js +119 -0
- package/engine/atomic/order-client.js +57 -0
- package/engine/atomic/order.js +132 -0
- package/engine/atomic/orderContext.js +70 -0
- package/engine/atomic/orderHalfCommit.js +29 -0
- package/engine/core/chainSource.js +306 -0
- package/engine/core/clock.js +40 -0
- package/engine/core/config.js +885 -0
- package/engine/core/finality.js +93 -0
- package/engine/core/format.js +168 -0
- package/engine/core/preSignGuards.js +495 -0
- package/engine/core/rfqSource.js +193 -0
- package/engine/core/sellWindow.js +43 -0
- package/engine/core/store.js +100 -0
- package/engine/core/swap.js +1153 -0
- package/engine/evm/amounts.js +131 -0
- package/engine/evm/auth.js +156 -0
- package/engine/evm/claimGas.js +87 -0
- package/engine/evm/depositor.js +127 -0
- package/engine/evm/dex.js +836 -0
- package/engine/evm/escrow.js +208 -0
- package/engine/evm/factory.js +149 -0
- package/engine/evm/fees.js +90 -0
- package/engine/evm/funding.js +309 -0
- package/engine/evm/gasReserve.js +88 -0
- package/engine/evm/index.js +123 -0
- package/engine/evm/injected.js +175 -0
- package/engine/evm/permit.js +125 -0
- package/engine/evm/prices.js +179 -0
- package/engine/evm/session.js +777 -0
- package/engine/mock/chain.js +98 -0
- package/engine/monero/address.js +265 -0
- package/engine/monero/halvesSweep.js +68 -0
- package/engine/monero/node.js +110 -0
- package/engine/monero/scan.js +325 -0
- package/engine/monero/swapKeys.js +69 -0
- package/engine/monero/unlock.js +46 -0
- package/engine/monero/wallet.js +168 -0
- package/engine/recovery/recoveryFile.js +363 -0
- package/engine/recovery/restore.js +176 -0
- package/engine/sdk/bridge.js +609 -0
- package/engine/ui/dom.js +90 -0
- package/engine/ui/walletModal.js +132 -0
- package/index.d.ts +1417 -0
- package/package.json +74 -0
- package/src/actions.mjs +72 -0
- package/src/adapters/evm-wallet.mjs +126 -0
- package/src/adapters/index.mjs +21 -0
- package/src/adapters/local-storage.mjs +32 -0
- package/src/adapters/monero-wallet.mjs +185 -0
- package/src/adapters/monero.mjs +51 -0
- package/src/adapters/node-test-evm-wallet.mjs +345 -0
- package/src/codes.mjs +188 -0
- package/src/config.mjs +277 -0
- package/src/engine.mjs +53 -0
- package/src/errors.mjs +24 -0
- package/src/http.mjs +72 -0
- package/src/index.mjs +256 -0
- package/src/legs/evm.mjs +118 -0
- package/src/legs/index.mjs +50 -0
- package/src/levelsWalkSpec.mjs +183 -0
- package/src/lock.mjs +140 -0
- package/src/order.mjs +212 -0
- package/src/preflight.mjs +137 -0
- package/src/primitives.mjs +47 -0
- package/src/quoteEip712.mjs +35 -0
- package/src/quoteEip712Spec.mjs +214 -0
- package/src/quotes.mjs +465 -0
- package/src/record.mjs +65 -0
- package/src/recovery.mjs +40 -0
- package/src/sim.mjs +37 -0
- package/src/swap-flow.mjs +492 -0
- package/src/swaps.mjs +935 -0
- package/src/sweep.mjs +222 -0
- package/src/wallet.mjs +164 -0
- package/src/xmr-arrival.mjs +289 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// THE SDK ENTRY POINT. Assembles the facades and holds rules common to all: no DOM, no texts, no windows.
|
|
2
|
+
import { buildConfig } from "./config.mjs";
|
|
3
|
+
import { createHttp } from "./http.mjs";
|
|
4
|
+
import { createQuotes } from "./quotes.mjs";
|
|
5
|
+
import { createPreflight } from "./preflight.mjs";
|
|
6
|
+
import { createRecovery } from "./recovery.mjs";
|
|
7
|
+
import { createSim } from "./sim.mjs";
|
|
8
|
+
import { createWallet } from "./wallet.mjs";
|
|
9
|
+
import { legFor } from "./legs/index.mjs";
|
|
10
|
+
import * as engine from "./engine.mjs";
|
|
11
|
+
import { createSwaps } from "./swaps.mjs";
|
|
12
|
+
// ORDER ACTIONS - ON THE FACADE: marking ready, claiming and refunding go through the same chain-read seam as
|
|
13
|
+
// the lock. The page gets them from the facade, not by importing the module separately.
|
|
14
|
+
import { createActions } from "./actions.mjs";
|
|
15
|
+
import { contextOf, verifyCounterparty, jointAddress } from "./order.mjs";
|
|
16
|
+
// THE DEAL ENGINE, NOW A PACKAGE MODULE (#32, wave 3): the order flow used to be an engine file the page pulled
|
|
17
|
+
// out of www/js. It is declared here so the facade carries the order READS the page needs (state and deadlines)
|
|
18
|
+
// on the same chain-read seam as the actions.
|
|
19
|
+
import * as orderFlow from "./swap-flow.mjs";
|
|
20
|
+
import { createSweep } from "./sweep.mjs";
|
|
21
|
+
import { SdkError, fail } from "./errors.mjs";
|
|
22
|
+
import { ERROR_CODES, STEP_CODES, CHECK_CODES } from "./codes.mjs";
|
|
23
|
+
|
|
24
|
+
// A surface declared by the contract but answering with a code: that is more honest than returning an empty
|
|
25
|
+
// value that looks like data. The stage number is named so the interface understands what to expect
|
|
26
|
+
// (sdk/README.md, "Stages").
|
|
27
|
+
const stageOf = {
|
|
28
|
+
"quotes.firm": 2,
|
|
29
|
+
"swaps.start": 2,
|
|
30
|
+
"swaps.list": 2,
|
|
31
|
+
"swaps.get": 2,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const notImplemented = (surface) => {
|
|
35
|
+
const error = new SdkError("not-implemented", { surface, stage: stageOf[surface] ?? null });
|
|
36
|
+
return async () => { throw error; };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const normalizeOptions = (options) => {
|
|
40
|
+
const o = options && typeof options === "object" ? options : {};
|
|
41
|
+
const settlement = o.settlement && typeof o.settlement === "object" ? o.settlement : null;
|
|
42
|
+
if (!settlement || typeof settlement.chain !== "string" || !settlement.chain) {
|
|
43
|
+
fail("bad-input", { field: "settlement.chain" });
|
|
44
|
+
}
|
|
45
|
+
if (settlement.vm !== undefined && (typeof settlement.vm !== "string" || !settlement.vm)) {
|
|
46
|
+
fail("bad-input", { field: "settlement.vm" });
|
|
47
|
+
}
|
|
48
|
+
if (typeof o.xmrNetwork !== "string" || !o.xmrNetwork) fail("bad-input", { field: "xmrNetwork" });
|
|
49
|
+
const mode = o.mode === undefined ? "live" : o.mode;
|
|
50
|
+
if (mode !== "live" && mode !== "sim") fail("bad-input", { field: "mode", known: ["live", "sim"] });
|
|
51
|
+
if (o.storage !== undefined) {
|
|
52
|
+
const s = o.storage;
|
|
53
|
+
if (!(s && typeof s.get === "function" && typeof s.set === "function" && typeof s.remove === "function")) {
|
|
54
|
+
fail("bad-input", { field: "storage" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (o.fetch !== undefined && typeof o.fetch !== "function") fail("bad-input", { field: "fetch" });
|
|
58
|
+
if (o.now !== undefined && typeof o.now !== "function") fail("bad-input", { field: "now" });
|
|
59
|
+
if (o.routing !== undefined) {
|
|
60
|
+
const r = o.routing;
|
|
61
|
+
if (!r || typeof r !== "object" || (r.prefer !== undefined && !Array.isArray(r.prefer))) fail("bad-input", { field: "routing" });
|
|
62
|
+
}
|
|
63
|
+
// PATHS AND ADDRESSES: the shape is checked here, the meaning - when the configuration is built (where the
|
|
64
|
+
// registry's defaults live). They must not be silently dropped: an override nobody knows about is worse than
|
|
65
|
+
// a refusal.
|
|
66
|
+
if (o.routes !== undefined && (!o.routes || typeof o.routes !== "object" || Array.isArray(o.routes))) {
|
|
67
|
+
fail("bad-input", { field: "routes" });
|
|
68
|
+
}
|
|
69
|
+
if (o.endpoints !== undefined && (!o.endpoints || typeof o.endpoints !== "object" || Array.isArray(o.endpoints))) {
|
|
70
|
+
fail("bad-input", { field: "endpoints" });
|
|
71
|
+
}
|
|
72
|
+
// THE WITNESS NODES FOR THE ARRIVAL CHECK: the list the interface or the user brings. A shape check only - the
|
|
73
|
+
// meaning (defaults, per-network) is worked out when the configuration is built. An array replaces the list for
|
|
74
|
+
// the chosen network, an object replaces per network, and `addNodes` appends (that is how a user adds their own).
|
|
75
|
+
if (o.nodes !== undefined && !Array.isArray(o.nodes) && (!o.nodes || typeof o.nodes !== "object")) {
|
|
76
|
+
fail("bad-input", { field: "nodes" });
|
|
77
|
+
}
|
|
78
|
+
if (o.addNodes !== undefined && !Array.isArray(o.addNodes)) fail("bad-input", { field: "addNodes" });
|
|
79
|
+
// THE CHAIN-READ SEAM. By default the core reads the chain through the page's wallet (the engine's
|
|
80
|
+
// readContract), and in Node there is no such wallet: a live run from Node was impossible because there was
|
|
81
|
+
// nothing to read predict/status/fee with. From outside you can bring your own reader - a READ, not a node
|
|
82
|
+
// substitution: the calls go to the network whose address the registry returned. The shape is the same as the
|
|
83
|
+
// lock seam: ({to, data}) -> hex.
|
|
84
|
+
if (o.evmCall !== undefined && typeof o.evmCall !== "function") fail("bad-input", { field: "evmCall" });
|
|
85
|
+
// THE CODE-READ SEAM (issue #103): ({address}) -> hex code, for the extcodehash check of the factory. Same
|
|
86
|
+
// rule as evmCall - a read brought from outside, not a substitution of the chain.
|
|
87
|
+
if (o.evmCode !== undefined && typeof o.evmCode !== "function") fail("bad-input", { field: "evmCode" });
|
|
88
|
+
// THE INTERFACE'S ALLOW-LIST (POLICY, NOT PROOF): an array of provider addresses (or providerIds) OR a
|
|
89
|
+
// predicate. It is NOT the check itself - that is `providerOf` on chain, taken from the quote - but the
|
|
90
|
+
// interface's own decision about whom it trades with. A wrong shape is a typo, not a silent no-op.
|
|
91
|
+
if (o.allowedProviders !== undefined && o.allowedProviders !== null) {
|
|
92
|
+
const a = o.allowedProviders;
|
|
93
|
+
if (!Array.isArray(a) && typeof a !== "function") fail("bad-input", { field: "allowedProviders" });
|
|
94
|
+
}
|
|
95
|
+
// THE INTERFACE'S FACTORY POLICY (issue #103): an array of code hashes (extcodehash) the interface accepts.
|
|
96
|
+
// Like allowedProviders - a POLICY list, not proof; empty/absent means "no policy".
|
|
97
|
+
if (o.knownFactoryCodes !== undefined && o.knownFactoryCodes !== null && !Array.isArray(o.knownFactoryCodes)) {
|
|
98
|
+
fail("bad-input", { field: "knownFactoryCodes" });
|
|
99
|
+
}
|
|
100
|
+
// A PASS TO OUR SERVICE - A SEAM FROM OUTSIDE, like the wallet: only a wallet can sign the sign-in message
|
|
101
|
+
// (www/js/evm/auth.js, signIn), and requiring it of the core would mean opening windows and signing messages
|
|
102
|
+
// inside the SDK. The core receives a ready pass and presents it. A missing pass is NOT an option error: the
|
|
103
|
+
// "server" step then answers with the app's refusal (401) under a named code.
|
|
104
|
+
if (o.serverAuth !== undefined && typeof o.serverAuth !== "function") fail("bad-input", { field: "serverAuth" });
|
|
105
|
+
// YOUR OWN SWAP RECORD - A CAPABILITY, NOT A REQUIREMENT. By default the core builds the record and the order
|
|
106
|
+
// side itself (record.mjs + order.mjs) and uses that side both for the record and for the lock terms. The
|
|
107
|
+
// function is for whoever wants to bring THEIR OWN side; it must be a function, otherwise it is a typo, not a
|
|
108
|
+
// setting.
|
|
109
|
+
if (o.createRecord !== undefined && typeof o.createRecord !== "function") fail("bad-input", { field: "createRecord" });
|
|
110
|
+
// WATCHING THE MONERO ADDRESS: the rate and the timeout. They are checked here by shape, and their meaning is
|
|
111
|
+
// worked out when the configuration is built (where the engine settings' defaults live).
|
|
112
|
+
if (o.watch !== undefined) {
|
|
113
|
+
const w = o.watch;
|
|
114
|
+
if (!w || typeof w !== "object" || Array.isArray(w)) fail("bad-input", { field: "watch" });
|
|
115
|
+
for (const name of ["pollMs", "timeoutMs"]) {
|
|
116
|
+
if (w[name] !== undefined && !(Number.isFinite(Number(w[name])) && Number(w[name]) > 0)) fail("bad-input", { field: "watch." + name });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
settlement: { chain: settlement.chain, vm: settlement.vm },
|
|
121
|
+
xmrNetwork: o.xmrNetwork,
|
|
122
|
+
apiBase: typeof o.apiBase === "string" && o.apiBase ? o.apiBase : "/api",
|
|
123
|
+
assetsBase: typeof o.assetsBase === "string" && o.assetsBase ? o.assetsBase : "/",
|
|
124
|
+
mode,
|
|
125
|
+
storage: o.storage || null,
|
|
126
|
+
fetch: o.fetch || null,
|
|
127
|
+
now: o.now || null,
|
|
128
|
+
routing: o.routing || null,
|
|
129
|
+
// PATHS AND ADDRESSES PASS ON AS THEY ARE: their meaning is worked out when the configuration is built,
|
|
130
|
+
// where the registry's defaults live. They must not be dropped here - an override nobody knows about is
|
|
131
|
+
// worse than a refusal.
|
|
132
|
+
routes: o.routes || null,
|
|
133
|
+
endpoints: o.endpoints || null,
|
|
134
|
+
nodes: o.nodes === undefined ? null : o.nodes,
|
|
135
|
+
addNodes: o.addNodes === undefined ? null : o.addNodes,
|
|
136
|
+
evmCall: o.evmCall || null,
|
|
137
|
+
evmCode: o.evmCode || null,
|
|
138
|
+
allowedProviders: o.allowedProviders === undefined ? null : o.allowedProviders,
|
|
139
|
+
knownFactoryCodes: o.knownFactoryCodes === undefined ? null : o.knownFactoryCodes,
|
|
140
|
+
serverAuth: o.serverAuth || null,
|
|
141
|
+
watch: o.watch || null,
|
|
142
|
+
createRecord: o.createRecord || null,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export function createNinsei(options) {
|
|
147
|
+
const o = normalizeOptions(options);
|
|
148
|
+
const config = buildConfig({
|
|
149
|
+
settlement: o.settlement,
|
|
150
|
+
xmrNetwork: o.xmrNetwork,
|
|
151
|
+
apiBase: o.apiBase,
|
|
152
|
+
assetsBase: o.assetsBase,
|
|
153
|
+
mode: o.mode,
|
|
154
|
+
routing: o.routing,
|
|
155
|
+
routes: o.routes,
|
|
156
|
+
endpoints: o.endpoints,
|
|
157
|
+
watch: o.watch,
|
|
158
|
+
nodes: o.nodes,
|
|
159
|
+
addNodes: o.addNodes,
|
|
160
|
+
});
|
|
161
|
+
const leg = legFor(config.settlement.vm);
|
|
162
|
+
const wallet = createWallet({ settlement: config.settlement.chain, leg });
|
|
163
|
+
// THE GUARDS READ THE NETWORK UNDER THE NAME evmNetwork, WHILE THE CONFIGURATION BUILDER RETURNS IT AS
|
|
164
|
+
// settlement.chain (preflight.mjs: both chain.nativeSymbol and config.evmNetwork.id). The names diverged, and
|
|
165
|
+
// ANY guard call failed with a TypeError - without a code, that is, not as a refusal but as a breakdown. A
|
|
166
|
+
// live swap start was therefore impossible in principle: preflight is called as the first step of start. We
|
|
167
|
+
// hand the guards a record of the same network under the name they expect; from outside it is the same config
|
|
168
|
+
// as before.
|
|
169
|
+
const preflight = createPreflight({ config: { ...config, evmNetwork: config.settlement.chain }, wallet, limits: config.limits });
|
|
170
|
+
const sim = o.mode === "sim" ? createSim() : null;
|
|
171
|
+
// THE STORAGE THE INTERFACE BROUGHT BECOMES THE CORE'S STORAGE: unfinished swaps and the agent session live
|
|
172
|
+
// where the interface decided (browser storage, memory, a server). Without the option the core takes its
|
|
173
|
+
// default: web storage if it is present in the environment, otherwise memory.
|
|
174
|
+
if (o.storage) {
|
|
175
|
+
engine.store.setStorage(o.storage);
|
|
176
|
+
engine.auth.setSessionStore(o.storage);
|
|
177
|
+
}
|
|
178
|
+
const http = createHttp({ apiBase: config.apiBase, fetchImpl: o.fetch, timeoutMs: config.limits.httpTimeoutMs });
|
|
179
|
+
// THE CHAIN READ AND THE ALLOW-LIST GO INTO THE QUOTE CHECK: without the read the signature certifies itself,
|
|
180
|
+
// so the same evmCall seam the lock and the actions use is handed to the quotes too.
|
|
181
|
+
const providerAllowed = (() => {
|
|
182
|
+
const a = o.allowedProviders;
|
|
183
|
+
if (a === null || a === undefined) return null;
|
|
184
|
+
if (typeof a === "function") return (provider) => a(provider) === true;
|
|
185
|
+
const set = new Set(a.map((v) => String(v).toLowerCase()));
|
|
186
|
+
return (provider) => set.has(String(provider).toLowerCase());
|
|
187
|
+
})();
|
|
188
|
+
const quotes = createQuotes({ config, http, now: o.now || (() => Date.now()), evmCall: o.evmCall || null,
|
|
189
|
+
evmCode: o.evmCode || null, providerAllowed, knownFactoryCodes: o.knownFactoryCodes });
|
|
190
|
+
// THE SAME CHAIN-READ SEAM AS THE LOCK AND THE CORE: checking the slots and signing must go one way, otherwise
|
|
191
|
+
// part of the check goes past the wallet and part does not.
|
|
192
|
+
const actions = createActions({ call: o.evmCall });
|
|
193
|
+
// THE SAME CHAIN-READ SEAM FOR THE ORDER READS: state and deadlines must be read by the wallet that will sign,
|
|
194
|
+
// not by a second provider. Without a caller reader the module keeps its engine default.
|
|
195
|
+
const orderDeps = typeof o.evmCall === "function" ? { call: o.evmCall } : {};
|
|
196
|
+
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
config,
|
|
199
|
+
wallet,
|
|
200
|
+
preflight,
|
|
201
|
+
recovery: createRecovery(),
|
|
202
|
+
|
|
203
|
+
// THE ORDER: checking the counterparty's side and the joint address. Both checks - BEFORE signing: after it
|
|
204
|
+
// there is nothing to fix.
|
|
205
|
+
// ORDER ACTIONS ON THE ESCROW: marking ready, claiming, refunding. Expectations (expect) are mandatory -
|
|
206
|
+
// without them there is nothing to check against, and the signature does not go out.
|
|
207
|
+
actions,
|
|
208
|
+
order: {
|
|
209
|
+
context: (terms) => contextOf(terms),
|
|
210
|
+
verifyCounterparty: (request) => verifyCounterparty(request),
|
|
211
|
+
jointAddress: (request) => jointAddress(request),
|
|
212
|
+
// ORDER STATE FROM THE CHAIN (escrow slot 3): which of the four states the order is in. Read, not guessed
|
|
213
|
+
// from logs - the very call the engine used to make from www/js/core/swap-flow.js.
|
|
214
|
+
status: (escrow) => orderFlow.orderStatus(escrow, orderDeps),
|
|
215
|
+
// THE ORDER DEADLINES (t1, readyBy, termsHash) from the chain: the screen needs them to say whether the
|
|
216
|
+
// person is in the dead zone.
|
|
217
|
+
deadlines: (escrow) => orderFlow.orderDeadlines(escrow, orderDeps),
|
|
218
|
+
},
|
|
219
|
+
sim,
|
|
220
|
+
quotes: {
|
|
221
|
+
// LIVE INDICATIVE QUOTES. The subscription delivers data on every poll tick and is removed by the function
|
|
222
|
+
// watch returned; the last listener to leave stops the polling.
|
|
223
|
+
watch: (request, onSnapshot) => quotes.watch(request, onSnapshot),
|
|
224
|
+
snapshot: () => quotes.snapshot(),
|
|
225
|
+
// A firm quote is stage 2: it is signed with a key and lives in the order protocol, not in a market snapshot.
|
|
226
|
+
// A FIRM QUOTE: accepted only with a confirmed provider signature.
|
|
227
|
+
firm: (request) => quotes.firm(request),
|
|
228
|
+
},
|
|
229
|
+
// READING STATE WORKS, STARTING IS NAMED HONESTLY: list/get raise swaps from storage after a page reload;
|
|
230
|
+
// start holds the step order and runs into the step that creates the record (stage 2).
|
|
231
|
+
// THE MODE AND THE MONERO NETWORK GO INTO THE SWAP START: in the sandbox the counterparty's side is built by
|
|
232
|
+
// a stand-in, on a live chain - only from the quote (a stand-in there would mean money against a
|
|
233
|
+
// non-existent side), and the network is needed to build the shared Monero address.
|
|
234
|
+
// THE CHAIN READER GOES INTO THE SWAP START: it will check the escrow address, the order state and the fee
|
|
235
|
+
// before signing.
|
|
236
|
+
// THE PATHS, THE POLL TIMINGS AND THE PASS GO INTO THE START: without the configuration the "server" step
|
|
237
|
+
// would not know where to knock, and without the pass there is nothing to present to the service (and that
|
|
238
|
+
// is a refusal with a CODE, not an invented path).
|
|
239
|
+
swaps: createSwaps({ preflight, http, mode: o.mode, xmrNetwork: o.xmrNetwork, call: o.evmCall,
|
|
240
|
+
config, auth: o.serverAuth, watch: o.watch,
|
|
241
|
+
// THE RECORD IS BUILT HERE ITSELF, and one brought from outside is only the caller's preference.
|
|
242
|
+
createRecord: o.createRecord }),
|
|
243
|
+
// XMR WITHDRAWAL: parsing the file and requesting data work; the withdrawal itself is named (the Monero
|
|
244
|
+
// wallet as an adapter).
|
|
245
|
+
sweep: createSweep({ http, config }),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export { SdkError, ERROR_CODES, STEP_CODES, CHECK_CODES };
|
|
250
|
+
|
|
251
|
+
// THE LEVEL WALK IS ON THE PACKAGE SURFACE. A maker publishes a level set (chunks of volume with their own prices)
|
|
252
|
+
// and both the node and the client compute a firm price by WALKING it - the client must reach the SAME price before
|
|
253
|
+
// signing. `levelsWalkSpec.mjs` is the ONE implementation, a byte-identical twin of the node's copy, and it is
|
|
254
|
+
// exported so a consumer can run the same walk: the set in, the answer out. Answers are values with a `reason`
|
|
255
|
+
// token on refusal, never exceptions.
|
|
256
|
+
export { checkLevels, xmrForAsset, assetForXmr, toBigInt, XMR_ATOMIC_PER_ONE } from "./levelsWalkSpec.mjs";
|
package/src/legs/evm.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// THE EVM LEG DRIVER. Everything the SDK knows about an EVM network as a settlement side.
|
|
2
|
+
//
|
|
3
|
+
// This is not the full leg yet: the escrow reads and writes (fund/markReady/claim/refund, the order state) will
|
|
4
|
+
// land here in stage 2. Already here: what the network is called for the wallet, how to tell that the wallet is on
|
|
5
|
+
// the right network, how balances are read, and the ROUTE-PROVIDER REGISTRY (our declared routes today, an
|
|
6
|
+
// external aggregator when it arrives).
|
|
7
|
+
export const vm = "evm";
|
|
8
|
+
|
|
9
|
+
/** What this network is called for the wallet: for EVM - a numeric chainId. */
|
|
10
|
+
export const walletChainId = (chain) => Number(chain.chainId);
|
|
11
|
+
|
|
12
|
+
/** Does the wallet's network match the settlement network. The comparison is VM-specific, so it lives in the driver. */
|
|
13
|
+
export const sameChain = (walletChainIdValue, chain) => {
|
|
14
|
+
const want = walletChainId(chain);
|
|
15
|
+
const got = Number(walletChainIdValue);
|
|
16
|
+
return Number.isFinite(got) && Number.isFinite(want) && got === want;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// THE balanceOf(address) SELECTOR - 0x70a08231. It is not taken from memory: the same value is declared in the
|
|
20
|
+
// project (rfq/evm.mjs, "verified against the project's code") and used by the engine (www/js/evm/session.js). The
|
|
21
|
+
// match is checked by tools/check-sdk.mjs - it reads the engine's registry and fails if they diverge.
|
|
22
|
+
export const BALANCE_OF_SELECTOR = "0x70a08231";
|
|
23
|
+
|
|
24
|
+
const toMinimal = (hex) => {
|
|
25
|
+
try {
|
|
26
|
+
return BigInt(hex).toString(10);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const addrWord = (address) => String(address).replace(/^0x/, "").toLowerCase().padStart(64, "0");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* BALANCES: the native coin - eth_getBalance, a token - balanceOf via eth_call. decimals are taken from the
|
|
35
|
+
* registry (they held the BSC trap), not from the chain's answer: the answer carries only the number in minimal
|
|
36
|
+
* units.
|
|
37
|
+
* Not read - null, not zero: zero would look like an empty wallet.
|
|
38
|
+
*/
|
|
39
|
+
export async function balances({ driver, address, tokens }) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const token of tokens) {
|
|
42
|
+
let amount = null;
|
|
43
|
+
try {
|
|
44
|
+
if (token.address === null) {
|
|
45
|
+
amount = toMinimal(await driver.request({ method: "eth_getBalance", params: [address, "latest"] }));
|
|
46
|
+
} else {
|
|
47
|
+
const data = BALANCE_OF_SELECTOR + addrWord(address);
|
|
48
|
+
amount = toMinimal(await driver.request({ method: "eth_call", params: [{ to: token.address, data }, "latest"] }));
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
amount = null;
|
|
52
|
+
}
|
|
53
|
+
out.push({ token: token.id, symbol: token.symbol, decimals: token.decimals, amount });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// THE ROUTE-PROVIDER REGISTRY (DEX/aggregator). Their role is one: turn what the person gives into what the escrow
|
|
59
|
+
// is funded with, and name the expected output. They DIFFER in the method: your own declared routes (DEX_ROUTES
|
|
60
|
+
// data in the engine's registry) or an external aggregator (1inch and the like).
|
|
61
|
+
//
|
|
62
|
+
// WHAT THIS GIVES: an aggregator becomes a CHOICE, not a rewrite - a new file and a line in this registry. The
|
|
63
|
+
// pre-signature guards are shared by all providers: the price is not market, the amount is covered, the token
|
|
64
|
+
// allowance - otherwise an external route would become a hole in those very guards.
|
|
65
|
+
//
|
|
66
|
+
// THE SHAPE OF THE SWAP IS DECLARED, NOT ASSUMED. A provider says which way its swap settles, because the deal
|
|
67
|
+
// path differs by it. Uniswap and SushiSwap are router AMMs - the router runs INSIDE the user's transaction, so
|
|
68
|
+
// the swap and the escrow funding are ONE transaction (shape "sync"). CoWSwap is an intent auction - the person
|
|
69
|
+
// signs an intent, and the settlement arrives LATER as a separate transaction sent by someone else (shape
|
|
70
|
+
// "async"). Hard-coding "one transaction" into the deal path would fit only the first and force a rewrite for the
|
|
71
|
+
// second, so the shape lives HERE and the path ASKS for it (see requireSyncProvider) instead of assuming it.
|
|
72
|
+
//
|
|
73
|
+
// AN ASYNCHRONOUS PROVIDER MUST DECLARE WHAT ENDS ITS EXECUTION - not a stub field, but something that can really
|
|
74
|
+
// be checked: `settled(request)`, a check that reads the outcome (for an intent auction - the settlement of the
|
|
75
|
+
// signed order) and answers whether the swap has ended and with what. A provider that cannot say this is
|
|
76
|
+
// incomplete, and the check refuses it by NAME rather than treating it as synchronous.
|
|
77
|
+
//
|
|
78
|
+
// REQUIREMENT FOR A PROVIDER: id, kind, shape ("sync" | "async"), plan(...) -> { ok, expectedOutWei | code,
|
|
79
|
+
// route?, calldata?, to? } and, for shape "async", settled(...). Execution (assembling and sending the
|
|
80
|
+
// transaction) is stage 3; for now only our provider is declared, and its plan honestly answers with a code.
|
|
81
|
+
export const SYNC = "sync";
|
|
82
|
+
export const ASYNC = "async";
|
|
83
|
+
|
|
84
|
+
const ROUTE_PROVIDERS = {
|
|
85
|
+
declared: {
|
|
86
|
+
id: "declared",
|
|
87
|
+
kind: "declared",
|
|
88
|
+
// OUR ROUTES ARE A ROUTER: the swap runs inside the user's own transaction and funds the escrow in it - hence
|
|
89
|
+
// "sync", and hence no completion step: there is no later action to wait for.
|
|
90
|
+
shape: SYNC,
|
|
91
|
+
async plan() {
|
|
92
|
+
return { ok: false, code: "not-implemented", stage: 3 };
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Who can execute a route at all. The list is needed by the "no such provider" error. */
|
|
98
|
+
export const routeProviders = () => Object.keys(ROUTE_PROVIDERS).sort();
|
|
99
|
+
export const routeProviderFor = (id) => ROUTE_PROVIDERS[id] || null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* THE GATE OF A PATH THAT ASSUMES "SWAP AND FUNDING IN ONE TRANSACTION". Such a path (stage 3: the router leg that
|
|
103
|
+
* funds the escrow with the very transaction that swaps) needs a provider whose swap happens in the user's OWN
|
|
104
|
+
* transaction. An asynchronous provider settles in a LATER, separate action, and silently treating it as
|
|
105
|
+
* synchronous would fund an escrow with nothing behind it - so it is refused BY NAME, with a `reason` token (the
|
|
106
|
+
* same shape of refusal the level book uses), not assumed to be synchronous.
|
|
107
|
+
*
|
|
108
|
+
* Returns { ok: true, provider } or { ok: false, reason, ... } - a refusal as a VALUE, not an exception: the
|
|
109
|
+
* caller keeps its own wording for the token.
|
|
110
|
+
*/
|
|
111
|
+
export const requireSyncProvider = (id) => {
|
|
112
|
+
const provider = routeProviderFor(id);
|
|
113
|
+
if (!provider) return { ok: false, reason: "provider-unknown", provider: typeof id === "string" ? id : null, known: routeProviders() };
|
|
114
|
+
if (provider.shape !== SYNC) {
|
|
115
|
+
return { ok: false, reason: "provider-not-synchronous", provider: provider.id, shape: provider.shape === undefined ? null : provider.shape };
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, provider };
|
|
118
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SETTLEMENT-LEG DRIVERS BY VIRTUAL MACHINE (VM).
|
|
2
|
+
//
|
|
3
|
+
// Why the seam was introduced right away, though one leg is implemented. Every VM has its own settlement side: for
|
|
4
|
+
// EVM it is a contract with an ABI and eth_* calls, for Tron its own contracts and its own resource model, for
|
|
5
|
+
// Solana a program and accounts. What they share is only the ROLE: deposit, mark ready, claim, refund, read
|
|
6
|
+
// state. So a "vm -> driver" map lives here, and the leg knows about this role, not about EVM: adding Tron or
|
|
7
|
+
// Solana means putting another file next to it and registering it HERE, touching neither the quotes, nor the
|
|
8
|
+
// Monero leg, nor the state machine.
|
|
9
|
+
//
|
|
10
|
+
// What must not be in a driver: no user strings, no DOM. A driver is chain reads and writes plus knowing what the
|
|
11
|
+
// network is measured in for this VM.
|
|
12
|
+
import * as evm from "./evm.mjs";
|
|
13
|
+
|
|
14
|
+
export const LEGS = { evm };
|
|
15
|
+
|
|
16
|
+
/** VMs for which a driver already exists. The list is needed by the check and by the "this network is not supported yet" error. */
|
|
17
|
+
export const implementedVms = () => Object.keys(LEGS).sort();
|
|
18
|
+
|
|
19
|
+
/** The leg driver or null. The caller must decide what to do with null: EVM must not be silently substituted. */
|
|
20
|
+
export const legFor = (vm) => LEGS[vm] || null;
|
|
21
|
+
|
|
22
|
+
// --- NETWORK -> VIRTUAL MACHINE -> DRIVER ------------------------------------------------------------
|
|
23
|
+
// The SDK keeps no network table: the source of truth is the engine's registry (www/js/core/config.js). A copy
|
|
24
|
+
// here would silently drift from it, and the swap would go to the wrong network.
|
|
25
|
+
import * as engine from "../engine.mjs";
|
|
26
|
+
import { fail } from "../errors.mjs";
|
|
27
|
+
|
|
28
|
+
/** A registry network by id, or null. */
|
|
29
|
+
export const chainOf = (chainId) => (engine.config.CHAINS || []).find((c) => c && c.id === chainId) || null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The network's virtual machine. An unknown network is null: "evm" must not be substituted by default, otherwise a
|
|
33
|
+
* network without a driver would travel the EVM path and refuse somewhere deep inside foreign code.
|
|
34
|
+
*/
|
|
35
|
+
export const vmOfChain = (chainId) => {
|
|
36
|
+
const chain = chainOf(chainId);
|
|
37
|
+
return chain && chain.vm ? chain.vm : null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The leg driver for a network. A network without a driver is a NAMED refusal: you can see which vm is not
|
|
42
|
+
* supported and which are. This is exactly the rule "no network is silently assumed to be EVM".
|
|
43
|
+
*/
|
|
44
|
+
export const legForChain = (chainId) => {
|
|
45
|
+
const vm = vmOfChain(chainId);
|
|
46
|
+
if (!vm) fail("bad-input", { field: "chain", chain: typeof chainId === "string" ? chainId : null });
|
|
47
|
+
const driver = legFor(vm);
|
|
48
|
+
if (!driver) fail("not-implemented", { surface: "legs", vm, implemented: implementedVms() });
|
|
49
|
+
return driver;
|
|
50
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// THE MAKER'S LEVEL SET: THE REFERENCE WALK OVER IT (issue #110, part B).
|
|
2
|
+
//
|
|
3
|
+
// WHY. The maker sends the node a LEVEL SET - chunks of volume with their own prices - and the node computes a
|
|
4
|
+
// firm price by WALKING it. The client (SDK) needs the same walk to show the same price before signing. This is
|
|
5
|
+
// the SINGLE implementation of the walk and of the level-set rules; the file exists as BYTE-IDENTICAL copies in
|
|
6
|
+
// two places, and tools/check-levels-walk.mjs verifies that:
|
|
7
|
+
// node: rfq/lib/levelsWalkSpec.mjs SDK: sdk/src/levelsWalkSpec.mjs
|
|
8
|
+
// Two copies, not an import: the node runs on the provider's machine and sees neither www/ nor sdk/, and the SDK
|
|
9
|
+
// package does not see rfq/ - each starts on its own (the same argument as rfq/lib/quoteEip712Spec.mjs). A
|
|
10
|
+
// divergence would mean the node and the client name a DIFFERENT price for the same set.
|
|
11
|
+
//
|
|
12
|
+
// NUMBERS ARE INTEGERS ONLY, BigInt ONLY (rule 10). XMR is in atomic units (1 XMR = 1e12). A price is in the
|
|
13
|
+
// asset's atomic units PER 1 WHOLE XMR (for a wei asset that is wei per 1 XMR). No Number for money.
|
|
14
|
+
//
|
|
15
|
+
// THE RULES OF THE SET (part B of the ticket):
|
|
16
|
+
// 1. a level is the NEXT chunk of volume, not a running total;
|
|
17
|
+
// 2. the best price comes FIRST, then only worse (for ask the best is the lowest, for bid the highest; checked
|
|
18
|
+
// by the same rule on the reversed set, the orientation argument); a violation is a NAMED refusal, not silence;
|
|
19
|
+
// 3. the first level sets the minimum deal size (there is no separate min);
|
|
20
|
+
// 4. an EMPTY set = the side is off, and that is NOT an error;
|
|
21
|
+
// 5. the walk goes BOTH ways: "I give asset - how much XMR" and "I give XMR - how much asset";
|
|
22
|
+
// 6. rounding to atomic units is in the maker's favour;
|
|
23
|
+
// 7. one orientation: volume in XMR, price in the asset per 1 XMR;
|
|
24
|
+
// 8. a fixed swap fee (fee in wei, separate for each side): the buyer pays the walk PLUS the fee, the seller
|
|
25
|
+
// receives the walk MINUS the fee. It is NOT spread over the volume: otherwise the average price would fall
|
|
26
|
+
// as the deal grew and the levels would break rule 2;
|
|
27
|
+
// 9. an order deeper than the side is a NAMED refusal; there is no partial fill.
|
|
28
|
+
//
|
|
29
|
+
// ROLES. role="buy" - the taker BUYS XMR and PAYS the asset (the fee is ADDED to the asset).
|
|
30
|
+
// role="sell" - the taker SELLS XMR and RECEIVES the asset (the fee is SUBTRACTED from the asset).
|
|
31
|
+
//
|
|
32
|
+
// REFUSALS COME BACK AS A reason TOKEN, NOT AS AN EXCEPTION: the caller must have a named refusal it can show
|
|
33
|
+
// in words, not a crash without a cause.
|
|
34
|
+
|
|
35
|
+
export const XMR_ATOMIC_PER_ONE = 1000000000000n;
|
|
36
|
+
|
|
37
|
+
// Parse an integer value into BigInt. Fractional / NaN / garbage -> null (the caller names the refusal, it does not guess).
|
|
38
|
+
export function toBigInt(value) {
|
|
39
|
+
if (typeof value === "bigint") return value;
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
return Number.isInteger(value) && Number.isSafeInteger(value) ? BigInt(value) : null;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
const s = value.trim();
|
|
45
|
+
if (/^[0-9]+$/.test(s)) return BigInt(s);
|
|
46
|
+
if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// CHECK THE SET. Returns a NAMED verdict (it does not throw):
|
|
52
|
+
// { ok: true, enabled: false } - empty set: the side is off (rule 4);
|
|
53
|
+
// { ok: true, enabled: true, count, minAtomic, totalAtomic } - the set is valid;
|
|
54
|
+
// { ok: false, reason, index } - the set breaks a rule.
|
|
55
|
+
export function checkLevels(levels, orientation = "ask") {
|
|
56
|
+
if (!Array.isArray(levels)) return { ok: false, reason: "levels-not-array" };
|
|
57
|
+
if (levels.length === 0) {
|
|
58
|
+
return { ok: true, enabled: false, count: 0, minAtomic: 0n, totalAtomic: 0n };
|
|
59
|
+
}
|
|
60
|
+
if (orientation !== "ask" && orientation !== "bid") return { ok: false, reason: "levels-bad-orientation" };
|
|
61
|
+
// ORIENTATION. Rule 2 ("the best price first, then only worse") reads DIFFERENTLY per side: for ask the best is
|
|
62
|
+
// the LOWEST, for bid the HIGHEST. So for bid the REVERSED set is checked - by the same device already used in
|
|
63
|
+
// the listener (levelOrderRefusal), not by a second rule beside it. The walk itself still runs over the ORIGINAL
|
|
64
|
+
// order: only the RULE is reversed, not the list (the best price must stay first for the walk).
|
|
65
|
+
const order = orientation === "bid" ? levels.slice().reverse() : levels;
|
|
66
|
+
let totalAtomic = 0n;
|
|
67
|
+
let prevPrice = null;
|
|
68
|
+
for (let index = 0; index < order.length; index += 1) {
|
|
69
|
+
const level = order[index];
|
|
70
|
+
if (!level || typeof level !== "object") return { ok: false, reason: "levels-bad-level", index };
|
|
71
|
+
const amount = toBigInt(level.amount);
|
|
72
|
+
const price = toBigInt(level.price);
|
|
73
|
+
if (amount === null || price === null) return { ok: false, reason: "levels-not-integer", index };
|
|
74
|
+
if (amount <= 0n || price <= 0n) return { ok: false, reason: "levels-not-positive", index };
|
|
75
|
+
// Equal prices are allowed (two chunks at one price are one level cut in two); a worse price is a NAMED
|
|
76
|
+
// refusal, and the index is translated back to the ORIGINAL numbering so the maker finds the guilty level.
|
|
77
|
+
if (prevPrice !== null && price < prevPrice) {
|
|
78
|
+
return { ok: false, reason: "levels-not-best-first", index: orientation === "bid" ? levels.length - 1 - index : index };
|
|
79
|
+
}
|
|
80
|
+
prevPrice = price;
|
|
81
|
+
totalAtomic += amount;
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, enabled: true, count: levels.length, minAtomic: toBigInt(levels[0].amount), totalAtomic };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// THE COST OF A CHUNK OF VOLUME IN WEI, ROUNDED UP (rule 6, in the maker's favour).
|
|
87
|
+
function levelCostWei(amountAtomic, priceWeiPerXmr) {
|
|
88
|
+
const num = amountAtomic * priceWeiPerXmr;
|
|
89
|
+
return (num + XMR_ATOMIC_PER_ONE - 1n) / XMR_ATOMIC_PER_ONE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// HOW MANY XMR ATOMS A BUDGET GIVES WITHIN ONE LEVEL, ROUNDED DOWN (rule 6).
|
|
93
|
+
function levelXmrForBudget(budgetWei, priceWeiPerXmr) {
|
|
94
|
+
return (budgetWei * XMR_ATOMIC_PER_ONE) / priceWeiPerXmr;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// THE FULL COST OF A SIDE (the sum of its levels' costs) and its volume - computed once.
|
|
98
|
+
function sideSummary(levels) {
|
|
99
|
+
let totalAtomic = 0n;
|
|
100
|
+
let fullCostWei = 0n;
|
|
101
|
+
for (const level of levels) {
|
|
102
|
+
const amount = toBigInt(level.amount);
|
|
103
|
+
const price = toBigInt(level.price);
|
|
104
|
+
totalAtomic += amount;
|
|
105
|
+
fullCostWei += levelCostWei(amount, price);
|
|
106
|
+
}
|
|
107
|
+
return { totalAtomic, fullCostWei };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// THE WALK WITHOUT THE FEE: a budget is given - how much XMR it yields (rounding down within a level, rule 6).
|
|
111
|
+
function walkXmrForBudget(levels, budgetWei) {
|
|
112
|
+
let remaining = budgetWei;
|
|
113
|
+
let xmrAtomic = 0n;
|
|
114
|
+
for (const level of levels) {
|
|
115
|
+
const amount = toBigInt(level.amount);
|
|
116
|
+
const price = toBigInt(level.price);
|
|
117
|
+
const full = levelCostWei(amount, price);
|
|
118
|
+
if (remaining >= full) {
|
|
119
|
+
xmrAtomic += amount;
|
|
120
|
+
remaining -= full;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
xmrAtomic += levelXmrForBudget(remaining, price);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
return xmrAtomic;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// THE WALK WITHOUT THE FEE: an XMR volume is given - what it costs (each chunk's cost rounded up, rule 6).
|
|
130
|
+
function walkCostForXmr(levels, xmrAtomic) {
|
|
131
|
+
let left = xmrAtomic;
|
|
132
|
+
let costWei = 0n;
|
|
133
|
+
for (const level of levels) {
|
|
134
|
+
if (left <= 0n) break;
|
|
135
|
+
const amount = toBigInt(level.amount);
|
|
136
|
+
const price = toBigInt(level.price);
|
|
137
|
+
const take = left < amount ? left : amount;
|
|
138
|
+
costWei += levelCostWei(take, price);
|
|
139
|
+
left -= take;
|
|
140
|
+
}
|
|
141
|
+
return costWei;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// DIRECTION 1: "I give/take ASSET - how much XMR".
|
|
145
|
+
// buy: assetWei - the asset the taker PAYS; the walk budget = assetWei - fee;
|
|
146
|
+
// sell: assetWei - the asset the taker RECEIVES; the walk budget = assetWei + fee (the set must yield that
|
|
147
|
+
// much so that AFTER the fee is taken out exactly assetWei is left in hand).
|
|
148
|
+
export function xmrForAsset({ levels, role, feeWei = 0n, assetWei, orientation = "ask" }) {
|
|
149
|
+
const verdict = checkLevels(levels, orientation);
|
|
150
|
+
if (!verdict.ok) return { ok: false, reason: verdict.reason, role, index: verdict.index };
|
|
151
|
+
if (!verdict.enabled) return { ok: false, reason: "side-off", role };
|
|
152
|
+
if (role !== "buy" && role !== "sell") return { ok: false, reason: "levels-bad-role", role };
|
|
153
|
+
const asset = toBigInt(assetWei);
|
|
154
|
+
const fee = toBigInt(feeWei === undefined || feeWei === null ? 0n : feeWei);
|
|
155
|
+
if (asset === null || fee === null || fee < 0n) return { ok: false, reason: "levels-not-integer", role };
|
|
156
|
+
const budget = role === "buy" ? asset - fee : asset + fee;
|
|
157
|
+
if (budget <= 0n) return { ok: false, reason: "levels-fee-beyond-budget", role };
|
|
158
|
+
const { fullCostWei } = sideSummary(levels);
|
|
159
|
+
if (budget > fullCostWei) return { ok: false, reason: "levels-exceed-side", role };
|
|
160
|
+
const minCost = levelCostWei(toBigInt(levels[0].amount), toBigInt(levels[0].price));
|
|
161
|
+
if (budget < minCost) return { ok: false, reason: "levels-below-minimum", role };
|
|
162
|
+
const xmrAtomic = walkXmrForBudget(levels, budget);
|
|
163
|
+
return { ok: true, role, xmrAtomic, assetWei: asset };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// DIRECTION 2: "I give/want XMR - how much ASSET".
|
|
167
|
+
// buy: xmrAtomic - how much XMR the taker WANTS; the asset to pay = the walk + fee;
|
|
168
|
+
// sell: xmrAtomic - how much XMR the taker GIVES; the asset to receive = the walk - fee.
|
|
169
|
+
export function assetForXmr({ levels, role, feeWei = 0n, xmrAtomic, orientation = "ask" }) {
|
|
170
|
+
const verdict = checkLevels(levels, orientation);
|
|
171
|
+
if (!verdict.ok) return { ok: false, reason: verdict.reason, role, index: verdict.index };
|
|
172
|
+
if (!verdict.enabled) return { ok: false, reason: "side-off", role };
|
|
173
|
+
if (role !== "buy" && role !== "sell") return { ok: false, reason: "levels-bad-role", role };
|
|
174
|
+
const want = toBigInt(xmrAtomic);
|
|
175
|
+
const fee = toBigInt(feeWei === undefined || feeWei === null ? 0n : feeWei);
|
|
176
|
+
if (want === null || fee === null || fee < 0n) return { ok: false, reason: "levels-not-integer", role };
|
|
177
|
+
if (want > verdict.totalAtomic) return { ok: false, reason: "levels-exceed-side", role };
|
|
178
|
+
if (want < verdict.minAtomic) return { ok: false, reason: "levels-below-minimum", role };
|
|
179
|
+
const walkWei = walkCostForXmr(levels, want);
|
|
180
|
+
const assetWei = role === "buy" ? walkWei + fee : walkWei - fee;
|
|
181
|
+
if (assetWei <= 0n) return { ok: false, reason: "levels-fee-beyond-budget", role };
|
|
182
|
+
return { ok: true, role, xmrAtomic: want, assetWei };
|
|
183
|
+
}
|