@ninsei-labs/sdk 0.42.0 → 0.42.1
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 +16 -0
- package/index.d.ts +24 -1
- package/package.json +3 -1
- package/src/levelsWalkSpec.mjs +39 -23
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.42.1 — 2026-10-12
|
|
2
|
+
|
|
3
|
+
THE BID SIDE OF THE QUOTE WALK ROUNDED THE WRONG WAY. The specification fixes a rounding DIRECTION per side, and
|
|
4
|
+
the walk rounded both sides alike: a bid price came out up to 3 wei high and disagreed with the reference at a
|
|
5
|
+
level boundary.
|
|
6
|
+
|
|
7
|
+
- Per-level rounding follows the side now: a level's cost rounds up on the ask side, where the user pays, and down
|
|
8
|
+
on the bid side, where the user is paid; the XMR amount a budget yields at a level rounds up on the bid side.
|
|
9
|
+
- The level-fits test is asymmetric: a level is taken whole when `remaining >= full` on the ask side and when
|
|
10
|
+
`remaining > full` on the bid side.
|
|
11
|
+
- This removes a systematic 1-to-3-wei over-charge on bid prices and the disagreement with the reference at level
|
|
12
|
+
boundaries.
|
|
13
|
+
|
|
14
|
+
Checks: `node tools/check.mjs` - all 19 green; `node tools/sync-version.mjs`, `node tools/npm-tag.test.mjs`, the two
|
|
15
|
+
teeth runs and `tools/sweep-refusal.test.mjs` are green too.
|
|
16
|
+
|
|
1
17
|
## 0.42.0 — 2026-10-12
|
|
2
18
|
|
|
3
19
|
EVERY ENTRY THIS PACKAGE PUBLISHES CARRIES ITS OWN DECLARATIONS, AND THE SWEEP PAGE'S TWO PRIMITIVES ARE ON
|
package/index.d.ts
CHANGED
|
@@ -500,7 +500,8 @@ export interface QuoteOffer {
|
|
|
500
500
|
* The providers already name them in their own tables (`COW_REFUSAL_CODES` and its three siblings), and the
|
|
501
501
|
* battery proves each of those is reachable; what was missing was the TYPE, so an interface could enumerate
|
|
502
502
|
* them instead of meeting a string it cannot switch on. `broken` is shared by all four: it is the code for a
|
|
503
|
-
* case a provider could not classify.
|
|
503
|
+
* case a provider could not classify. `RouteRefusalTables` below declares what those tables ARE - each of them a
|
|
504
|
+
* `readonly RouteRefusalCode[]` - so the union is the tables' TYPE and not a comment standing beside them.
|
|
504
505
|
*
|
|
505
506
|
* These are OUR codes, and they are what a person's message should be phrased from. A provider's OWN reason
|
|
506
507
|
* stays opaque on purpose (`QuoteRefusal.why`): that is data the provider sent, not a name of ours, so it is
|
|
@@ -546,6 +547,28 @@ export type RouteRefusalCode =
|
|
|
546
547
|
// shared
|
|
547
548
|
| "broken";
|
|
548
549
|
|
|
550
|
+
/**
|
|
551
|
+
* WHAT THE FOUR REFUSAL TABLES *ARE*. Each route provider names its own refusals in one table - `COW_REFUSAL_CODES`
|
|
552
|
+
* (src/legs/cow.mjs), `KYBER_REFUSAL_CODES` (src/legs/kyber.mjs), `UNISWAPX_REFUSAL_CODES` (src/legs/uniswapx.mjs) and
|
|
553
|
+
* `UNISWAPRELAY_REFUSAL_CODES` (src/legs/uniswaprelay.mjs) - and every one of those tables is a
|
|
554
|
+
* `readonly RouteRefusalCode[]`. The dependency runs ONE WAY on purpose: a table answers to the union (a code that is
|
|
555
|
+
* not declared above cannot enter a table), while the union answers to no single table, so the same vocabulary is
|
|
556
|
+
* met in four modules.
|
|
557
|
+
*
|
|
558
|
+
* THEY ARE NOT ON THIS ENTRY'S SURFACE, and this is deliberately NOT a value: the package re-exports no provider and
|
|
559
|
+
* no table (src/index.mjs), the tables travel in the shipped source (`files` in package.json) and they are the
|
|
560
|
+
* VOCABULARY, not an API. A `declare const` here that the runtime does not export would be a lie, and the `surface`
|
|
561
|
+
* check refuses exactly that - so the tables are declared as their TYPE. That is what keeps the union from being a
|
|
562
|
+
* comment beside the tables: this declaration says what they are, and the `refusals` check says the same thing from
|
|
563
|
+
* the other side - a table declared here as a plain `readonly string[]` is a named problem there.
|
|
564
|
+
*/
|
|
565
|
+
export interface RouteRefusalTables {
|
|
566
|
+
readonly COW_REFUSAL_CODES: readonly RouteRefusalCode[];
|
|
567
|
+
readonly KYBER_REFUSAL_CODES: readonly RouteRefusalCode[];
|
|
568
|
+
readonly UNISWAPX_REFUSAL_CODES: readonly RouteRefusalCode[];
|
|
569
|
+
readonly UNISWAPRELAY_REFUSAL_CODES: readonly RouteRefusalCode[];
|
|
570
|
+
}
|
|
571
|
+
|
|
549
572
|
export interface QuoteRefusal {
|
|
550
573
|
readonly providerId: string;
|
|
551
574
|
readonly displayName: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninsei-labs/sdk",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"check:secrets": "node tools/check.mjs --only=secrets",
|
|
21
21
|
"check:types": "node tools/check.mjs --only=types",
|
|
22
22
|
"check:types-teeth": "node tools/check-types-teeth.mjs",
|
|
23
|
+
"check:refusals": "node tools/check.mjs --only=refusals",
|
|
24
|
+
"check:refusals-teeth": "node tools/check-refusals-teeth.mjs",
|
|
23
25
|
"check:sweep": "node tools/sweep-refusal.test.mjs", "sync:version": "node tools/sync-version.mjs"
|
|
24
26
|
},
|
|
25
27
|
"types": "./index.d.ts",
|
package/src/levelsWalkSpec.mjs
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
// 3. the first level sets the minimum deal size (there is no separate min);
|
|
20
20
|
// 4. an EMPTY set = the side is off, and that is NOT an error;
|
|
21
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
|
|
22
|
+
// 6. rounding to atomic units is in the maker's favour, EACH LEVEL ON ITS OWN: up on ask, where the user pays,
|
|
23
|
+
// down on bid, where the user is paid; the answer in XMR is rounded the maker's way too, down for a buyer
|
|
24
|
+
// (ask) and up for a seller (bid);
|
|
23
25
|
// 7. one orientation: volume in XMR, price in the asset per 1 XMR;
|
|
24
26
|
// 8. a fixed swap fee (fee in wei, separate for each side): the buyer pays the walk PLUS the fee, the seller
|
|
25
27
|
// receives the walk MINUS the fee. It is NOT spread over the volume: otherwise the average price would fall
|
|
@@ -83,51 +85,65 @@ export function checkLevels(levels, orientation = "ask") {
|
|
|
83
85
|
return { ok: true, enabled: true, count: levels.length, minAtomic: toBigInt(levels[0].amount), totalAtomic };
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// ROUND A NON-NEGATIVE FRACTION TO A WHOLE UNIT: up, or down. Money is BigInt only (rule 10); the fraction is
|
|
89
|
+
// non-negative where this is reached (amounts and prices are positive, a budget is positive).
|
|
90
|
+
function divRound(num, den, up) {
|
|
91
|
+
const q = num / den;
|
|
92
|
+
return up && q * den < num ? q + 1n : q;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
// THE COST OF A CHUNK OF VOLUME IN THE ASSET, ONE LEVEL AT A TIME (rule 6). Rounding is always in the maker's
|
|
96
|
+
// favour: UP on ask, where the user pays, DOWN on bid, where the node pays the user. A walk rounds each level on
|
|
97
|
+
// its own and adds the levels up, as the node does: rounding the sum once would differ by up to a unit per level,
|
|
98
|
+
// and the node would sign the maker another amount than its journal books.
|
|
99
|
+
function levelCostWei(amountAtomic, priceWeiPerXmr, orientation) {
|
|
100
|
+
return divRound(amountAtomic * priceWeiPerXmr, XMR_ATOMIC_PER_ONE, orientation === "ask");
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
//
|
|
98
|
-
|
|
103
|
+
// HOW MANY XMR ATOMS A BUDGET GIVES WITHIN ONE LEVEL (rule 6). The answer in XMR is rounded the maker's way too:
|
|
104
|
+
// DOWN for a buyer (ask), UP for a seller (bid). At a coarse asset several atomic units pay the same, and a
|
|
105
|
+
// seller sends no more of them than the payout needs.
|
|
106
|
+
function levelXmrForBudget(budgetWei, priceWeiPerXmr, orientation) {
|
|
107
|
+
return divRound(budgetWei * XMR_ATOMIC_PER_ONE, priceWeiPerXmr, orientation === "bid");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// THE FULL COST OF A SIDE (the sum of its levels' costs, each rounded on its own) and its volume - computed once.
|
|
111
|
+
function sideSummary(levels, orientation) {
|
|
99
112
|
let totalAtomic = 0n;
|
|
100
113
|
let fullCostWei = 0n;
|
|
101
114
|
for (const level of levels) {
|
|
102
115
|
const amount = toBigInt(level.amount);
|
|
103
116
|
const price = toBigInt(level.price);
|
|
104
117
|
totalAtomic += amount;
|
|
105
|
-
fullCostWei += levelCostWei(amount, price);
|
|
118
|
+
fullCostWei += levelCostWei(amount, price, orientation);
|
|
106
119
|
}
|
|
107
120
|
return { totalAtomic, fullCostWei };
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
// THE WALK WITHOUT THE FEE: a budget is given - how much XMR it yields (
|
|
111
|
-
function walkXmrForBudget(levels, budgetWei) {
|
|
123
|
+
// THE WALK WITHOUT THE FEE: a budget is given - how much XMR it yields (each level rounded on its own, rule 6).
|
|
124
|
+
function walkXmrForBudget(levels, budgetWei, orientation) {
|
|
112
125
|
let remaining = budgetWei;
|
|
113
126
|
let xmrAtomic = 0n;
|
|
114
127
|
for (const level of levels) {
|
|
115
128
|
const amount = toBigInt(level.amount);
|
|
116
129
|
const price = toBigInt(level.price);
|
|
117
|
-
const full = levelCostWei(amount, price);
|
|
118
|
-
|
|
130
|
+
const full = levelCostWei(amount, price, orientation);
|
|
131
|
+
// The maker's way once more: a buyer (ask) takes a level whole even when the budget only just reaches it; a
|
|
132
|
+
// seller (bid) sends no more than the payout needs, so an exact match is left to the partial step, rounded up.
|
|
133
|
+
const fits = orientation === "ask" ? remaining >= full : remaining > full;
|
|
134
|
+
if (fits) {
|
|
119
135
|
xmrAtomic += amount;
|
|
120
136
|
remaining -= full;
|
|
121
137
|
continue;
|
|
122
138
|
}
|
|
123
|
-
xmrAtomic += levelXmrForBudget(remaining, price);
|
|
139
|
+
xmrAtomic += levelXmrForBudget(remaining, price, orientation);
|
|
124
140
|
break;
|
|
125
141
|
}
|
|
126
142
|
return xmrAtomic;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
// THE WALK WITHOUT THE FEE: an XMR volume is given - what it costs (each chunk's cost rounded
|
|
130
|
-
function walkCostForXmr(levels, xmrAtomic) {
|
|
145
|
+
// THE WALK WITHOUT THE FEE: an XMR volume is given - what it costs (each chunk's cost rounded on its own, rule 6).
|
|
146
|
+
function walkCostForXmr(levels, xmrAtomic, orientation) {
|
|
131
147
|
let left = xmrAtomic;
|
|
132
148
|
let costWei = 0n;
|
|
133
149
|
for (const level of levels) {
|
|
@@ -135,7 +151,7 @@ function walkCostForXmr(levels, xmrAtomic) {
|
|
|
135
151
|
const amount = toBigInt(level.amount);
|
|
136
152
|
const price = toBigInt(level.price);
|
|
137
153
|
const take = left < amount ? left : amount;
|
|
138
|
-
costWei += levelCostWei(take, price);
|
|
154
|
+
costWei += levelCostWei(take, price, orientation);
|
|
139
155
|
left -= take;
|
|
140
156
|
}
|
|
141
157
|
return costWei;
|
|
@@ -166,16 +182,16 @@ export function xmrForAsset({ levels, role, feeWei = 0n, assetWei, orientation =
|
|
|
166
182
|
if (asset === null || fee === null || fee < 0n) return { ok: false, reason: "levels-not-integer", role };
|
|
167
183
|
const budget = role === "buy" ? asset - fee : asset + fee;
|
|
168
184
|
if (budget <= 0n) return { ok: false, reason: "levels-fee-beyond-budget", role };
|
|
169
|
-
const { fullCostWei } = sideSummary(levels);
|
|
185
|
+
const { fullCostWei } = sideSummary(levels, orientation);
|
|
170
186
|
if (budget > fullCostWei) return { ok: false, reason: "levels-exceed-side", role };
|
|
171
187
|
// THE SMALLEST TRADE, from the set AS THE MAKER SENT IT (rule 3): callers pass it in minAtomic and the
|
|
172
188
|
// node's deductions never change it. Absent, the set's own first level is used, as it always was.
|
|
173
189
|
const min = minOf(levels, minAtomic);
|
|
174
190
|
// Its cost ON THE EFFECTIVE SET: after deductions the smallest trade may reach into a later level, so the
|
|
175
191
|
// cost is walked, not read off one price.
|
|
176
|
-
const minCost = walkCostForXmr(levels, min);
|
|
192
|
+
const minCost = walkCostForXmr(levels, min, orientation);
|
|
177
193
|
if (budget < minCost) return { ok: false, reason: "levels-below-minimum", role };
|
|
178
|
-
const xmrAtomic = walkXmrForBudget(levels, budget);
|
|
194
|
+
const xmrAtomic = walkXmrForBudget(levels, budget, orientation);
|
|
179
195
|
return { ok: true, role, xmrAtomic, assetWei: asset };
|
|
180
196
|
}
|
|
181
197
|
|
|
@@ -193,7 +209,7 @@ export function assetForXmr({ levels, role, feeWei = 0n, xmrAtomic, orientation
|
|
|
193
209
|
if (want > verdict.totalAtomic) return { ok: false, reason: "levels-exceed-side", role };
|
|
194
210
|
// THE SMALLEST TRADE AS THE MAKER SENT IT, never the effective set's first level (rule 3): see minOf.
|
|
195
211
|
if (want < minOf(levels, minAtomic)) return { ok: false, reason: "levels-below-minimum", role };
|
|
196
|
-
const walkWei = walkCostForXmr(levels, want);
|
|
212
|
+
const walkWei = walkCostForXmr(levels, want, orientation);
|
|
197
213
|
const assetWei = role === "buy" ? walkWei + fee : walkWei - fee;
|
|
198
214
|
if (assetWei <= 0n) return { ok: false, reason: "levels-fee-beyond-budget", role };
|
|
199
215
|
return { ok: true, role, xmrAtomic: want, assetWei };
|
package/src/version.mjs
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
// tools/sync-version.mjs. Do not edit it by hand - set the version in package.json and run `npm run sync:version`.
|
|
7
7
|
// The `version` check in tools/check.mjs refuses a mismatch, and refuses a runtime read of package.json here.
|
|
8
8
|
|
|
9
|
-
export const SDK_VERSION = "0.42.
|
|
9
|
+
export const SDK_VERSION = "0.42.1";
|