@ninsei-labs/sdk 0.49.0 → 0.50.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 +12 -0
- package/index.d.ts +5 -0
- package/package.json +1 -1
- package/src/actions.mjs +15 -0
- package/src/quotes.mjs +33 -2
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.50.0 — 2026-10-21
|
|
2
|
+
|
|
3
|
+
A CLAIM IS SENT BY ITS CLAIMER, AND A SALE IS RANKED AFTER THE TIP.
|
|
4
|
+
|
|
5
|
+
- `actions.claim` compares the wallet's address with `request.claimer` or `expect.claimer` before it signs. A
|
|
6
|
+
third-party wallet, a missing claimer or a wallet without an address is a named `bad-input`; `claimSenderVerdict`
|
|
7
|
+
exposes the same rule. This is a deliberate tightening: a claim reveals the half to its sender.
|
|
8
|
+
- `readQuote` keeps the signed `claimTip`, and `rankOffers` selects a sale by `amount × rate − claimTip`, not by
|
|
9
|
+
the headline rate. A buy, or a request without an amount, still ranks by rate.
|
|
10
|
+
- `quotes.watch` uses that ranking for the chosen offer, so moving margin from the price into the tip no longer
|
|
11
|
+
wins a sale.
|
|
12
|
+
|
|
1
13
|
## 0.49.0 — 2026-10-21
|
|
2
14
|
|
|
3
15
|
A ONE-KEY RECOVERY CAN WITHDRAW WITHOUT A FULL SCAN, AND A CLAIM TIP IS CHECKED.
|
package/index.d.ts
CHANGED
|
@@ -507,6 +507,8 @@ export interface QuoteOffer {
|
|
|
507
507
|
readonly currency: string | null;
|
|
508
508
|
readonly rate: number;
|
|
509
509
|
readonly rateQuote: number;
|
|
510
|
+
/** The claim tip in native-token wei; a sale is ranked by the payout AFTER it. */
|
|
511
|
+
readonly claimTip: string;
|
|
510
512
|
readonly min: number | null;
|
|
511
513
|
readonly max: number | null;
|
|
512
514
|
readonly step: number | null;
|
|
@@ -1597,6 +1599,9 @@ export interface ActionsApi {
|
|
|
1597
1599
|
export interface ActionsRequest {
|
|
1598
1600
|
readonly escrow: string;
|
|
1599
1601
|
readonly expect: Readonly<Record<string, unknown>>;
|
|
1602
|
+
/** The claimer whose half is revealed by `claim`; absent - `expect.claimer` is used. A claim without one is
|
|
1603
|
+
* refused: the package sends it only from the claimer's own wallet, never from a third-party sender. */
|
|
1604
|
+
readonly claimer?: string;
|
|
1600
1605
|
readonly onStep?: (stage: string, detail?: string) => void;
|
|
1601
1606
|
}
|
|
1602
1607
|
|
package/package.json
CHANGED
package/src/actions.mjs
CHANGED
|
@@ -25,6 +25,18 @@ const walletOrFail = (wallet) => {
|
|
|
25
25
|
return wallet;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
// THE CLAIM'S HALF IS REVEALED BY WHOEVER SENDS IT. The package sends a claim only from the claimer's own wallet:
|
|
29
|
+
// a third-party sender would see the half before the claim is mined, and the only reason to use one is the tip -
|
|
30
|
+
// not a reason to hand the half to another party. A caller with no claimer is refused, not sent blind.
|
|
31
|
+
export function claimSenderVerdict({ sender = null, claimer = null } = {}) {
|
|
32
|
+
const from = typeof sender === "string" ? sender.trim().toLowerCase() : "";
|
|
33
|
+
const to = typeof claimer === "string" ? claimer.trim().toLowerCase() : "";
|
|
34
|
+
if (!to) return { ok: false, code: "bad-input", field: "claimer" };
|
|
35
|
+
if (!from) return { ok: false, code: "bad-input", field: "wallet.address" };
|
|
36
|
+
if (from !== to) return { ok: false, code: "bad-input", field: "wallet", why: "not-the-claimer" };
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
const requestOrFail = (request, extra) => {
|
|
29
41
|
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
30
42
|
const missing = missingFields(request, ["escrow", "expect", ...extra]);
|
|
@@ -59,6 +71,9 @@ export function createActions({ call = null } = {}) {
|
|
|
59
71
|
requestOrFail(request, ["halfClaimer"]);
|
|
60
72
|
walletOrFail(wallet);
|
|
61
73
|
const { escrow, halfClaimer, expect, onStep } = request;
|
|
74
|
+
const sender = typeof wallet.address === "function" ? await wallet.address() : null;
|
|
75
|
+
const verdict = claimSenderVerdict({ sender, claimer: request.claimer || (expect && expect.claimer) });
|
|
76
|
+
if (!verdict.ok) throw new SdkError(verdict.code, { field: verdict.field, why: verdict.why, surface: "actions.claim" });
|
|
62
77
|
return await withCodes("claim", () => flow.claimOrder({ escrow, halfClaimer, expect, onStep, deps: deps(wallet) }));
|
|
63
78
|
},
|
|
64
79
|
/** Refunding: it also reveals a half. The core passes the expectations ALWAYS, though the engine does not require them. */
|
package/src/quotes.mjs
CHANGED
|
@@ -83,6 +83,13 @@ export function readQuote(pair, raw, owner = {}, data = {}) {
|
|
|
83
83
|
currencyNetwork: q.currencyNetwork ? String(q.currencyNetwork) : null,
|
|
84
84
|
rateQuote,
|
|
85
85
|
rate,
|
|
86
|
+
// THE CLAIM TIP IS IN THE MAKER'S NATIVE TOKEN (wei). A sale is ranked by what the seller receives AFTER it;
|
|
87
|
+
// the value is kept as a string, because a wei amount does not fit a float reliably.
|
|
88
|
+
claimTip: (() => {
|
|
89
|
+
if (q.claimTip === undefined || q.claimTip === null || q.claimTip === "") return "0";
|
|
90
|
+
try { const n = BigInt(q.claimTip); return n < 0n ? "0" : n.toString(); }
|
|
91
|
+
catch { return "0"; }
|
|
92
|
+
})(),
|
|
86
93
|
min: num(q.min),
|
|
87
94
|
max: num(q.max),
|
|
88
95
|
step: num(q.step),
|
|
@@ -104,6 +111,27 @@ export function readQuote(pair, raw, owner = {}, data = {}) {
|
|
|
104
111
|
};
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
/**
|
|
115
|
+
* THE BEST OFFER IS THE ONE THAT PAYS THE MOST, NOT THE ONE WITH THE HIGHEST RATE. On a sale the maker may move
|
|
116
|
+
* its margin into `claimTip`: the escrow pays that tip out of the seller's payout, so a higher rate with a larger
|
|
117
|
+
* tip can leave the seller with less. With no amount there is nothing to compare beyond the rate, and a buy has
|
|
118
|
+
* no tip. Native-token wei are converted at 18 decimals: a sale settles in the chain's native coin.
|
|
119
|
+
*/
|
|
120
|
+
export function rankOffers(offers = [], { direction = "buy", amount = null } = {}) {
|
|
121
|
+
const list = Array.isArray(offers) ? offers.filter((offer) => offer && Number(offer.rate) > 0) : [];
|
|
122
|
+
const amountXmr = Number(amount);
|
|
123
|
+
const score = (offer) => {
|
|
124
|
+
const rate = Number(offer.rate);
|
|
125
|
+
if (direction !== "sell" || !(amountXmr > 0)) return rate;
|
|
126
|
+
let tip = 0;
|
|
127
|
+
try { tip = Number(BigInt(offer.claimTip || "0")) / 1e18; } catch { tip = 0; }
|
|
128
|
+
return amountXmr * rate - tip;
|
|
129
|
+
};
|
|
130
|
+
const ranked = list.map((offer) => ({ offer, net: score(offer) }))
|
|
131
|
+
.sort((a, b) => (b.net - a.net) || (Number(b.offer.rate) - Number(a.offer.rate)));
|
|
132
|
+
return { best: ranked.length ? ranked[0].offer : null, net: ranked.length ? ranked[0].net : null, ranked };
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
/**
|
|
108
136
|
* Parsing the book in one place. The envelope is accepted in three forms (a map by pairs, a list of rows, a list
|
|
109
137
|
* of providers) and is brought to a single list: the screen must not know which form the answer came in.
|
|
@@ -201,7 +229,7 @@ const compactOffer = (o) => ({
|
|
|
201
229
|
assetNetwork: o.assetNetwork, currencyNetwork: o.currencyNetwork, networkStated: o.networkStated,
|
|
202
230
|
networksDerived: o.networksDerived === true, networksUnnamed: o.networksUnnamed === true,
|
|
203
231
|
// THE REASON (in the provider's words), THE PACKAGE'S SIGNATURE AND KEY, THE AGE BY ITS OWN MARK.
|
|
204
|
-
why: o.why, signature: o.signature, keyId: o.keyId, ageMs: o.ageMs,
|
|
232
|
+
claimTip: o.claimTip, why: o.why, signature: o.signature, keyId: o.keyId, ageMs: o.ageMs,
|
|
205
233
|
networks: { asset: o.assetNetwork, currency: o.currencyNetwork, stated: o.networkStated },
|
|
206
234
|
});
|
|
207
235
|
|
|
@@ -267,8 +295,11 @@ export function createQuotes({ config, http, now = () => Date.now(), evmCall = n
|
|
|
267
295
|
const asked = explicit ? Number(request.size) : size;
|
|
268
296
|
const data = await http.json("/rate?amount=" + encodeURIComponent(asked) + "&side=" + encodeURIComponent(request.direction));
|
|
269
297
|
const book = readBook(data, now());
|
|
270
|
-
const offer = book.best;
|
|
271
298
|
const amount = Number.isFinite(Number(request.amount)) ? Number(request.amount) : null;
|
|
299
|
+
// A SALE IS RANKED BY THE PAYOUT AFTER THE TIP; a buy has no tip. Without an amount the rate is the only
|
|
300
|
+
// comparable number, and the service's own best remains the fallback.
|
|
301
|
+
const ranked = rankOffers(book.offers, { direction: request.direction, amount });
|
|
302
|
+
const offer = ranked.best || book.best;
|
|
272
303
|
// The size derived from the person's amount becomes the size of the NEXT request: the price does not depend
|
|
273
304
|
// on volume, and the volume is needed by the provider to say whether it takes such a size. With a named size
|
|
274
305
|
// there is nothing to derive - it stays what it was asked as.
|
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.
|
|
9
|
+
export const SDK_VERSION = "0.50.0";
|