@ninsei-labs/sdk 0.51.1 → 0.51.2
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 +8 -0
- package/README.md +1 -1
- package/engine/core/chainSource.js +4 -60
- package/engine/core/config.js +8 -5
- package/engine/core/swap.js +9 -215
- package/engine/sdk/bridge.js +1 -1
- package/index.d.ts +1 -23
- package/package.json +1 -1
- package/src/http.mjs +2 -0
- package/src/index.mjs +1 -1
- package/src/swaps.mjs +27 -16
- package/src/sweep-restore.mjs +28 -10
- package/src/sweep.mjs +8 -57
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.51.2 — 2026-10-22
|
|
2
|
+
|
|
3
|
+
- The withdrawal window falls back to the full scan when `scanTxs` or the balance read fails, and reports
|
|
4
|
+
`window.fallbackWhy: "window-scan"`.
|
|
5
|
+
- The SDK does not call a backend by default: the `/api/swaps` lookups, the `swept` report, the session calls and the
|
|
6
|
+
relative `apiBase` default are gone. A caller that needs a backend passes its base explicitly; a watch without
|
|
7
|
+
`verify` data is a named `no-backend` refusal.
|
|
8
|
+
|
|
1
9
|
## 0.51.1 — 2026-10-22
|
|
2
10
|
|
|
3
11
|
A BUSY NODE NO LONGER ENDS A WITHDRAWAL, AND MISSING HEIGHT DATA HAS A NAME.
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ import { createNinsei } from "@ninsei-labs/sdk";
|
|
|
44
44
|
const sdk = createNinsei({
|
|
45
45
|
settlement: { chain: "arbitrum-sepolia" }, // from sdk.config.chains; vm is taken from the registry
|
|
46
46
|
xmrNetwork: "stagenet",
|
|
47
|
-
apiBase: "
|
|
47
|
+
// apiBase: "https://your-service", // only when you bring a backend; there is no default
|
|
48
48
|
assetsBase: "/sdk/",
|
|
49
49
|
mode: "live", // or "sim"
|
|
50
50
|
});
|
|
@@ -67,6 +67,7 @@ export function pollInterval() {
|
|
|
67
67
|
// see the reason, not a blank page.
|
|
68
68
|
async function api(path, { method = "GET", body, base, timeoutMs = API.timeoutMs, fetchImpl } = {}) {
|
|
69
69
|
const target = base || apiBaseOverride || API.base;
|
|
70
|
+
if (!target) return { ok: false, status: 0, error: "no backend base configured" };
|
|
70
71
|
const doFetch = fetchImpl || globalThis.fetch;
|
|
71
72
|
if (typeof doFetch !== "function") return { ok: false, status: 0, error: "no fetch: backend unavailable" };
|
|
72
73
|
const ctl = typeof AbortController === "function" ? new AbortController() : null;
|
|
@@ -142,66 +143,9 @@ export function sameSwapTarget(a, b) {
|
|
|
142
143
|
a.chain === b.chain;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const viewKey = src.viewKey || src.privateViewKey || null;
|
|
149
|
-
if (!address || !viewKey) {
|
|
150
|
-
return { ok: false, status: 0, error: "the watch-only share has no address/viewKey - nothing to send to the backend" };
|
|
151
|
-
}
|
|
152
|
-
// THE EXPECTED AMOUNT IS MANDATORY and must be a number. Before, a non-number meant no field, and the swap
|
|
153
|
-
// was created WITHOUT an amount: any arrival was accepted as enough, so an underpayment looked like a done
|
|
154
|
-
// deal. Now it is a refusal with an explanation.
|
|
155
|
-
const amountXmr = Number(expectedAmountXmr);
|
|
156
|
-
if (!Number.isFinite(amountXmr) || amountXmr <= 0) {
|
|
157
|
-
const where = (new Error("caller").stack || "").split("\n").slice(1, 4).map((x) => x.trim().split("/").pop()).join(" <- ");
|
|
158
|
-
console.warn("[chainSource] no expected amount, call from: " + where);
|
|
159
|
-
return { ok: false, status: 0, error: "the expected XMR amount is unknown (" + String(expectedAmountXmr) + "): without it no watch is created" };
|
|
160
|
-
}
|
|
161
|
-
const wanted = {
|
|
162
|
-
swapId: swapId || src.swapId || null,
|
|
163
|
-
network: src.network || xmrNetworkForSource(),
|
|
164
|
-
address,
|
|
165
|
-
viewKey,
|
|
166
|
-
expectedAmountXmr: amountXmr,
|
|
167
|
-
restoreHeight: Number.isFinite(Number(restoreHeight)) && Number(restoreHeight) > 0 ? Number(restoreHeight) : Number(src.restoreHeight) || undefined,
|
|
168
|
-
chain: chain || undefined,
|
|
169
|
-
};
|
|
170
|
-
// The body is rebuilt from the allowlist: what is not in the list never reaches the request.
|
|
171
|
-
const body = {};
|
|
172
|
-
for (const field of ALLOWED_SESSION_FIELDS) {
|
|
173
|
-
if (wanted[field] !== undefined && wanted[field] !== null) body[field] = wanted[field];
|
|
174
|
-
}
|
|
175
|
-
const res = await api("/swaps", { method: "POST", body, ...opts });
|
|
176
|
-
if (!res.ok) return res;
|
|
177
|
-
return { ok: true, status: res.status, created: res.json?.created !== false, session: normalizeSession(res.json) };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export async function fetchSession(swapId, opts = {}) {
|
|
181
|
-
const res = await api(`/swaps/${encodeURIComponent(swapId)}`, opts);
|
|
182
|
-
if (!res.ok) return res;
|
|
183
|
-
return { ok: true, status: res.status, session: normalizeSession(res.json) };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Tell the backend the XMR was withdrawn (the hash of the real transfer). Needed because a watch-only wallet
|
|
187
|
-
// cannot see outgoing transfers.
|
|
188
|
-
export async function reportSwept(swapId, txid, opts = {}) {
|
|
189
|
-
const res = await api(`/swaps/${encodeURIComponent(swapId)}/swept`, { method: "POST", body: { txid }, ...opts });
|
|
190
|
-
return res.ok ? { ok: true, status: res.status, session: normalizeSession(res.json) } : res;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Close the swap (cancel or refund on the backend): the watch stops.
|
|
194
|
-
export async function closeSession(swapId, reason = "cancelled", opts = {}) {
|
|
195
|
-
const res = await api(`/swaps/${encodeURIComponent(swapId)}/closed`, { method: "POST", body: { reason }, ...opts });
|
|
196
|
-
return res.ok ? { ok: true, status: res.status, session: normalizeSession(res.json) } : res;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Backend state: is it up and does it see the wallet. The screen needs this to show 'backend unavailable'
|
|
200
|
-
// before the user sends money, not after.
|
|
201
|
-
export async function health(opts = {}) {
|
|
202
|
-
const res = await api("/health", opts);
|
|
203
|
-
return res.ok ? { ok: true, ...res.json } : res;
|
|
204
|
-
}
|
|
146
|
+
// NO SESSION CALLS. The swap engine no longer creates a watch on a backend: the address and the view key are not
|
|
147
|
+
// sent to the page's own host, and the SDK does not poll a session for the Monero leg. A caller that needs a
|
|
148
|
+
// backend later passes its base explicitly, outside these rules.
|
|
205
149
|
|
|
206
150
|
// ---------------------------------------------------------------------------
|
|
207
151
|
// The Monero-leg snapshot for the state engine
|
package/engine/core/config.js
CHANGED
|
@@ -908,14 +908,17 @@ export const MONERO = {
|
|
|
908
908
|
};
|
|
909
909
|
|
|
910
910
|
// ---------------------------------------------------------------------------
|
|
911
|
-
//
|
|
911
|
+
// A backend is OPTIONAL (app/): the SDK does not point requests at the page's own origin.
|
|
912
912
|
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
913
|
+
// A caller that needs one passes `apiBase` explicitly; without it the swap engine watches the chain and the maker,
|
|
914
|
+
// not a service. The legacy session calls were removed for that reason.
|
|
915
915
|
// ---------------------------------------------------------------------------
|
|
916
916
|
export const API = {
|
|
917
|
-
base
|
|
918
|
-
//
|
|
917
|
+
// NO BACKEND BY DEFAULT. A page that needs one passes the base explicitly; nothing in the SDK points requests at
|
|
918
|
+
// the page's own origin. The legacy session calls were removed for the same reason: a host could link the visitor
|
|
919
|
+
// to the escrow from a request the widget never needed.
|
|
920
|
+
base: null,
|
|
921
|
+
// The Monero-leg source: 'app' - a backend the caller explicitly configured, 'mock' - the demo simulator.
|
|
919
922
|
xmrSource: "app",
|
|
920
923
|
pollMs: 5_000, // how often the front end polls a swap session
|
|
921
924
|
// HOW MANY PROVIDERS TO SHOW in the price block. A list of dozens of rows is unreadable, and a person cannot
|
package/engine/core/swap.js
CHANGED
|
@@ -29,7 +29,7 @@ import * as node from "../monero/node.js";
|
|
|
29
29
|
// The Monero-leg source: in "app" mode confirmations and arrivals come from the backend,
|
|
30
30
|
// in "mock" mode - from the demo simulator. Exactly what goes outward (only the address and view key)
|
|
31
31
|
// is checked mechanically.
|
|
32
|
-
import { XMR_STATUS, explorerXmrTx,
|
|
32
|
+
import { XMR_STATUS, explorerXmrTx, isLiveSource, xmrView } from "./chainSource.js";
|
|
33
33
|
import { watchOnlyShareFromWallet } from "../recovery/restore.js";
|
|
34
34
|
|
|
35
35
|
export const STEP_LABELS = [
|
|
@@ -788,25 +788,6 @@ function emit() {
|
|
|
788
788
|
}
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
-
// --- live mode: the swap session on the backend -----------------------------------------------
|
|
792
|
-
//
|
|
793
|
-
// The poll runs without waiting: the screen ticker must not wait for the network, else with an unavailable backend
|
|
794
|
-
// the demo starts to lag. The result is put into the swap itself (xmrSession / xmrSessionError), and derive()
|
|
795
|
-
// reads it as ordinary state - it stays a pure function.
|
|
796
|
-
const liveInFlight = new Set();
|
|
797
|
-
|
|
798
|
-
function applyLive(id, patch) {
|
|
799
|
-
const s = loadState();
|
|
800
|
-
if (!s.swaps[id]) return;
|
|
801
|
-
Object.assign(s.swaps[id], patch);
|
|
802
|
-
saveState(s);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// The time of the last ATTEMPT to reach the backend per swap. Attempts, not successes: rate limiting must work
|
|
806
|
-
// even when the request fails.
|
|
807
|
-
const liveLastTry = new Map();
|
|
808
|
-
|
|
809
|
-
|
|
810
791
|
// THE DEAL KEY: THE PAGE AND THE APP NAME IT DIFFERENTLY. The page has its own id (lives in the browser), the
|
|
811
792
|
// app its own (localtest-...): the id is set by the app when it creates the watch record. The page asked for the
|
|
812
793
|
// XMR state by ITS OWN id, which the app does not know and answers with a refusal. From outside it looks like
|
|
@@ -830,188 +811,21 @@ export function dealSwapIds(swap) {
|
|
|
830
811
|
return [...ids];
|
|
831
812
|
}
|
|
832
813
|
|
|
833
|
-
const serverIdByAddress = new Map();
|
|
834
|
-
|
|
835
|
-
// WE LOOK FOR THE DEAL'S FACTS ACROSS ALL ITS RECORDS IN THE BROWSER. The page keeps TWO records per deal: one
|
|
836
|
-
// created by the swap (Monero address, expected XMR amount), another by the escrow (halves, escrow address). A
|
|
837
|
-
// screen opened on the escrow record did not know the expected amount, the session was not created at all and the
|
|
838
|
-
// readiness button did not turn on - although the app was already reporting readiness. The records are tied by the SHARED Monero address: one per deal.
|
|
839
|
-
function dealFactsFor(swap) {
|
|
840
|
-
const w0 = (() => { try { return halvesWalletOf(swap) || {}; } catch { return {}; } })();
|
|
841
|
-
let address = w0.address || (swap && swap.escrow && swap.escrow.moneroAddress) || (swap && swap.xmrSession && swap.xmrSession.address) || null;
|
|
842
|
-
let amountXmr = Number(swap && swap.xmrAmount) > 0 ? Number(swap.xmrAmount) : null;
|
|
843
|
-
let source = "own record";
|
|
844
|
-
if (address && amountXmr === null) {
|
|
845
|
-
for (const other of Object.values((loadState() || {}).swaps || {})) {
|
|
846
|
-
if (!other || other === swap) continue;
|
|
847
|
-
const w = (() => { try { return halvesWalletOf(other) || {}; } catch { return {}; } })();
|
|
848
|
-
if (!w.address || w.address !== address) continue;
|
|
849
|
-
if (Number(other.xmrAmount) > 0) { amountXmr = Number(other.xmrAmount); source = "another record of the same deal (" + String(other.id || "?") + ")"; break; }
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
if (!address) {
|
|
853
|
-
for (const other of Object.values((loadState() || {}).swaps || {})) {
|
|
854
|
-
const w = (() => { try { return halvesWalletOf(other) || {}; } catch { return {}; } })();
|
|
855
|
-
if (w.address && Number(other.xmrAmount) > 0) { address = w.address; amountXmr = amountXmr || Number(other.xmrAmount); source = "another record of the same deal"; break; }
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return { address, amountXmr, source };
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// WE TAKE THE DEAL ADDRESS FROM EVERYWHERE IT MAY LIE. A browser deal and the app's watch record are DIFFERENT
|
|
862
|
-
// records about one deal (the page creates its own for the escrow, the app one for watching the address), so the
|
|
863
|
-
// page's record does not always have the address: on the escrow record it lies in the halves, not in the
|
|
864
|
-
// escrow.moneroAddress field. A live run therefore polled a dead id, got 404 and left the readiness button off.
|
|
865
|
-
// Extracted separately so the same address is used by the server height lookup too.
|
|
866
|
-
function dealMoneroAddress(swap) {
|
|
867
|
-
const fromHalves = (() => { try { return (halvesWalletOf(swap) || {}).address || null; } catch { return null; } })();
|
|
868
|
-
return (swap && swap.escrow && swap.escrow.moneroAddress)
|
|
869
|
-
|| (swap && swap.xmrSession && swap.xmrSession.address)
|
|
870
|
-
|| (swap && swap.swapWallet && swap.swapWallet.address)
|
|
871
|
-
|| fromHalves
|
|
872
|
-
|| null;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
814
|
// THE SCAN HEIGHT THE APP KNOWS. Needed by OLD deals' files: their record has no birthHeight (the field appeared
|
|
876
815
|
// later), while the app resolved and stored the height when registering the watch. The source is ours, so it does
|
|
877
816
|
// not depend on someone else's Monero node. The server is silent - we return null and the caller keeps the
|
|
878
817
|
// previous behaviour: the height comes from other sources or the file honestly warns of its absence.
|
|
879
|
-
export async function serverRestoreHeightFor(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
//
|
|
883
|
-
// hair earlier: then the fallback source is silent and null goes into the file - and there is nowhere else to
|
|
884
|
-
// get the height, the file is already with the owner. A few short attempts close this gap instead of turning
|
|
885
|
-
// it into an empty field; the price is fractions of a second in the rare case when there is no row at all.
|
|
886
|
-
for (let i = 0; i < attempts; i++) {
|
|
887
|
-
try {
|
|
888
|
-
const r = await fetch("/api/swaps", { headers: { accept: "application/json" } });
|
|
889
|
-
if (r.ok) {
|
|
890
|
-
const j = await r.json();
|
|
891
|
-
const rows = Array.isArray(j) ? j : (j.swaps || []);
|
|
892
|
-
const row = rows.find((x) => String((x && x.address) || "") === String(addr));
|
|
893
|
-
const h = Number(row && row.restoreHeight);
|
|
894
|
-
if (Number.isFinite(h) && h > 0) return h;
|
|
895
|
-
}
|
|
896
|
-
} catch {
|
|
897
|
-
/* try again: the service may have been busy with the first request */
|
|
898
|
-
}
|
|
899
|
-
if (i < attempts - 1) await new Promise((r) => setTimeout(r, pauseMs));
|
|
900
|
-
}
|
|
818
|
+
export async function serverRestoreHeightFor() {
|
|
819
|
+
// NO BACKEND LOOKUP. The app's watch record is not a source for the SDK any more: asking the page's own host by
|
|
820
|
+
// the deal's Monero address is exactly the request the owner removed. A caller that still has a backend passes the
|
|
821
|
+
// height itself; otherwise the recovery file says it is missing.
|
|
901
822
|
return null;
|
|
902
823
|
}
|
|
903
824
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (!addr) return null;
|
|
907
|
-
if (serverIdByAddress.has(addr)) return serverIdByAddress.get(addr);
|
|
908
|
-
try {
|
|
909
|
-
const r = await fetch("/api/swaps", { headers: { accept: "application/json" } });
|
|
910
|
-
if (!r.ok) return null;
|
|
911
|
-
const j = await r.json();
|
|
912
|
-
const rows = Array.isArray(j) ? j : (j.swaps || []);
|
|
913
|
-
const row = rows.find((x) => String((x && x.address) || "") === String(addr));
|
|
914
|
-
if (!row || !row.id) return null;
|
|
915
|
-
serverIdByAddress.set(addr, String(row.id));
|
|
916
|
-
return String(row.id);
|
|
917
|
-
} catch {
|
|
918
|
-
return null; // the server is silent - we keep the previous behaviour, the screen does not wait for the network
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function refreshLiveSession(id) {
|
|
923
|
-
if (liveInFlight.has(id)) return;
|
|
924
|
-
// No more than once per pollMs per swap, REGARDLESS of whether the session survived. The limit used to stand
|
|
925
|
-
// only in the "session exists" branch: if the request failed (429 from the proxy, 502 from a timeout), the
|
|
926
|
-
// session was not saved, the limit did not apply - and the tick repeated the request every 5 seconds for EACH
|
|
927
|
-
// swap. With eight accumulated swaps that produced a stream of 429s: the app fought itself.
|
|
928
|
-
const now = Date.now();
|
|
929
|
-
const lastTry = liveLastTry.get(id) || 0;
|
|
930
|
-
if (now - lastTry < pollInterval()) return;
|
|
931
|
-
liveLastTry.set(id, now);
|
|
932
|
-
liveInFlight.add(id);
|
|
933
|
-
(async () => {
|
|
934
|
-
try {
|
|
935
|
-
const fresh = loadState().swaps[id];
|
|
936
|
-
if (!fresh || fresh.settlement) return;
|
|
937
|
-
if (!fresh.xmrSession) {
|
|
938
|
-
// The swap wallet is created in the browser; only the address and view key go outward (see chainSource).
|
|
939
|
-
// THE EXPECTED AMOUNT IS MANDATORY. Without it we do not create a session at all: the wallet is the same
|
|
940
|
-
// as on the confirmation screen, so the address is already watched. Otherwise a second truth about one
|
|
941
|
-
// deal would appear at the same address, and a record without an amount would let an underpayment through.
|
|
942
|
-
// As soon as the amount appears, the next tick creates the session itself.
|
|
943
|
-
const facts = dealFactsFor(fresh);
|
|
944
|
-
const amountXmr = facts.amountXmr;
|
|
945
|
-
if (!Number.isFinite(amountXmr) || amountXmr <= 0) {
|
|
946
|
-
pushLog(fresh, simElapsed(fresh), "Backend session not opened yet: amount of XMR is unknown", String((fresh && fresh.xmrAmount) ?? "no"));
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
if (facts.source !== "own record") {
|
|
950
|
-
pushLog(fresh, simElapsed(fresh), "Amount of XMR taken from the deal's other record", facts.source + ", " + String(amountXmr));
|
|
951
|
-
}
|
|
952
|
-
const share = watchOnlyShareFromWallet({ swapId: fresh.id, wallet: fresh.swapWallet });
|
|
953
|
-
const opened = await openSession({
|
|
954
|
-
swapId: fresh.id,
|
|
955
|
-
share,
|
|
956
|
-
expectedAmountXmr: amountXmr,
|
|
957
|
-
chain: fresh.network,
|
|
958
|
-
});
|
|
959
|
-
if (opened.ok) {
|
|
960
|
-
applyLive(id, { xmrSession: opened.session, xmrSessionError: null });
|
|
961
|
-
const s2 = loadState();
|
|
962
|
-
if (s2.swaps[id]) {
|
|
963
|
-
pushLog(s2.swaps[id], simElapsed(s2.swaps[id]), "Backend session opened (watch-only: address + view key)", opened.session.address || "");
|
|
964
|
-
saveState(s2);
|
|
965
|
-
}
|
|
966
|
-
} else {
|
|
967
|
-
applyLive(id, { xmrSessionError: opened.error });
|
|
968
|
-
}
|
|
969
|
-
} else if (Date.now() - (fresh.xmrSession.at || 0) >= pollInterval()) {
|
|
970
|
-
// WE ASK BY THE APP'S ID, not our own: see resolveServerSwapId. If the record was not found, we do NOT
|
|
971
|
-
// ask by our own id - the app will answer 404 and the screen will silently wait forever. Instead we
|
|
972
|
-
// create the watch afresh: the app itself ties it by address to the existing record and returns the same
|
|
973
|
-
// one, not a second truth about one deal.
|
|
974
|
-
const serverId = await resolveServerSwapId(fresh);
|
|
975
|
-
const polled = await fetchSession(serverId || id);
|
|
976
|
-
if (polled.ok) {
|
|
977
|
-
applyLive(id, { xmrSession: polled.session, xmrSessionError: null });
|
|
978
|
-
} else if (!serverId) {
|
|
979
|
-
const share = watchOnlyShareFromWallet({ swapId: fresh.id, wallet: fresh.swapWallet });
|
|
980
|
-
const again = await openSession({
|
|
981
|
-
swapId: fresh.id, share, expectedAmountXmr: Number(fresh.xmrAmount), chain: fresh.network,
|
|
982
|
-
});
|
|
983
|
-
if (again.ok) {
|
|
984
|
-
applyLive(id, { xmrSession: again.session, xmrSessionError: null });
|
|
985
|
-
const s3 = loadState();
|
|
986
|
-
if (s3.swaps[id]) { pushLog(s3.swaps[id], simElapsed(s3.swaps[id]), "Backend record re-linked by the Monero address", again.session.address || ""); saveState(s3); }
|
|
987
|
-
} else {
|
|
988
|
-
applyLive(id, { xmrSessionError: again.error });
|
|
989
|
-
}
|
|
990
|
-
} else {
|
|
991
|
-
applyLive(id, { xmrSessionError: polled.error });
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
} catch (e) {
|
|
995
|
-
applyLive(id, { xmrSessionError: "session did not open: " + e.message });
|
|
996
|
-
} finally {
|
|
997
|
-
liveInFlight.delete(id);
|
|
998
|
-
}
|
|
999
|
-
})();
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// The deals currently open on screen. The core polls the backend ONLY for them.
|
|
1003
|
-
//
|
|
1004
|
-
// Why: the tick used to poll all unfinished deals indiscriminately. After a few presses of the sign button they
|
|
1005
|
-
// accumulated to eight, and the app itself flooded the proxy with requests - a 429 came back. And on the main
|
|
1006
|
-
// page there is nothing to watch at all: there is no deal yet.
|
|
1007
|
-
//
|
|
1008
|
-
// An empty set (the default) means "poll nothing". Screens that need watching report their set themselves: the
|
|
1009
|
-
// deal list - all unfinished, the deal screen - one.
|
|
1010
|
-
let liveFocusIds = new Set();
|
|
1011
|
-
|
|
825
|
+
// Kept for callers that used to name the deals on screen. There is no backend poll any more; the function only
|
|
826
|
+
// returns how many ids were named, so an old caller does not break.
|
|
1012
827
|
export function setLiveFocus(ids) {
|
|
1013
|
-
|
|
1014
|
-
return liveFocusIds.size;
|
|
828
|
+
return new Set(Array.isArray(ids) ? ids.map(String) : []).size;
|
|
1015
829
|
}
|
|
1016
830
|
|
|
1017
831
|
export function tick() {
|
|
@@ -1024,7 +838,6 @@ export function tick() {
|
|
|
1024
838
|
// Live mode: a session on the backend. We create it on the first tick after the swap is created and then
|
|
1025
839
|
// poll no more often than API.pollMs. Not awaited: the screen must not wait for the network.
|
|
1026
840
|
// We poll only what is open on screen.
|
|
1027
|
-
if (isLiveSource(swap.xmrSource) && liveFocusIds.has(String(swap.id))) refreshLiveSession(swap.id);
|
|
1028
841
|
|
|
1029
842
|
const view = derive(swap);
|
|
1030
843
|
|
|
@@ -1080,26 +893,7 @@ export function tick() {
|
|
|
1080
893
|
changed = true;
|
|
1081
894
|
}
|
|
1082
895
|
|
|
1083
|
-
// Live mode: the
|
|
1084
|
-
// transfers), so the final state is taken from the session status, and the hash is not invented.
|
|
1085
|
-
if (isLiveSource(swap.xmrSource) && swap.xmrSession && swap.xmrSession.status === XMR_STATUS.SWEPT && !swap.settlement) {
|
|
1086
|
-
const atSim = view.sim;
|
|
1087
|
-
swap.settlement = {
|
|
1088
|
-
kind: "success",
|
|
1089
|
-
phase: TERMINAL.success.phase,
|
|
1090
|
-
atSim,
|
|
1091
|
-
txid: swap.xmrSession.sweepTxid || null,
|
|
1092
|
-
by: "your sweep wallet",
|
|
1093
|
-
};
|
|
1094
|
-
pushLog(swap, atSim, "XMR swept by you (backend reported)", swap.settlement.txid || "");
|
|
1095
|
-
const key = swap.id + ":success";
|
|
1096
|
-
if (!notified.has(key)) {
|
|
1097
|
-
notified.add(key);
|
|
1098
|
-
notify("Swap " + swap.id + " complete: XMR is on your address", "ok");
|
|
1099
|
-
}
|
|
1100
|
-
changed = true;
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
896
|
+
// Live mode: the XMR arrives from the chain; the SDK does not confirm a sweep from a backend session.
|
|
1103
897
|
|
|
1104
898
|
// happy path: after claim the user's wallet sweeps the XMR itself (litepaper §4.2)
|
|
1105
899
|
if (!isLiveSource(swap.xmrSource) && view.can.sweep && view.phase === "success") {
|
package/engine/sdk/bridge.js
CHANGED
|
@@ -122,7 +122,7 @@ function optionsFor(chain) {
|
|
|
122
122
|
return {
|
|
123
123
|
settlement: { chain: chain.id },
|
|
124
124
|
xmrNetwork: MONERO.networkType,
|
|
125
|
-
|
|
125
|
+
// NO BACKEND BY DEFAULT: a page that needs one passes its base explicitly.
|
|
126
126
|
assetsBase: "/",
|
|
127
127
|
mode: modeOf(chain),
|
|
128
128
|
// THE PAGE STORAGE BECOMES THE CORE STORAGE: deal records and the session live in one place.
|
package/index.d.ts
CHANGED
|
@@ -1303,14 +1303,8 @@ export interface SweepApi {
|
|
|
1303
1303
|
* the person what will leave, BEFORE sending.
|
|
1304
1304
|
*/
|
|
1305
1305
|
sign(request: SweepRunRequest): Promise<SweepSignResult>;
|
|
1306
|
-
/** Send what `sign` signed, close the wallet and
|
|
1306
|
+
/** Send what `sign` signed, close the wallet and return the transaction hashes. */
|
|
1307
1307
|
relay(request: SweepRelayRequest): Promise<SweepRunResult>;
|
|
1308
|
-
/**
|
|
1309
|
-
* TELL THE SERVICE ABOUT THE WITHDRAWAL. The mark does not affect the transfer - it is already on the network - so
|
|
1310
|
-
* it can be repeated: the interface calls this even after `relay` if the report did not go through then. A
|
|
1311
|
-
* connection refusal gives `reported: false`.
|
|
1312
|
-
*/
|
|
1313
|
-
report(request: SweepReportRequest): Promise<SweepReportResult>;
|
|
1314
1308
|
/** Perform the withdrawal as a whole (`sign` + `relay`). */
|
|
1315
1309
|
run(request: SweepRunRequest): Promise<SweepRunResult>;
|
|
1316
1310
|
}
|
|
@@ -1421,16 +1415,6 @@ export interface SweepWallet {
|
|
|
1421
1415
|
driver(): unknown;
|
|
1422
1416
|
}
|
|
1423
1417
|
|
|
1424
|
-
/** The withdrawal mark: the swap identifier and the transfer hash. */
|
|
1425
|
-
export interface SweepReportRequest {
|
|
1426
|
-
readonly swapId: string;
|
|
1427
|
-
readonly txid: string;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
export interface SweepReportResult {
|
|
1431
|
-
readonly reported: boolean;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
1418
|
export interface SweepFromOrderRequest {
|
|
1435
1419
|
/** The 12 words of the recovery key. */
|
|
1436
1420
|
readonly mnemonic: string;
|
|
@@ -1585,12 +1569,6 @@ export interface SweepRelayRequest {
|
|
|
1585
1569
|
export interface SweepRunResult {
|
|
1586
1570
|
readonly txids: readonly string[];
|
|
1587
1571
|
readonly swept: string;
|
|
1588
|
-
/**
|
|
1589
|
-
* Whether the service was told about the withdrawal. THE REPORT DOES NOT AFFECT THE TRANSFER - it is already on
|
|
1590
|
-
* the network - so a failed report does not make the withdrawal unsuccessful: it gives `reported: false`, so the
|
|
1591
|
-
* interface offers to report again.
|
|
1592
|
-
*/
|
|
1593
|
-
readonly reported: boolean;
|
|
1594
1572
|
}
|
|
1595
1573
|
|
|
1596
1574
|
// --- Recovery// --- Recovery ------------------------------------------------------------------------------
|
package/package.json
CHANGED
package/src/http.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export function createHttp({ apiBase, fetchImpl = null, timeoutMs = 20_000 }) {
|
|
|
16
16
|
// a GET, and the service would answer with a refusal to a request it did not understand. The defaults are
|
|
17
17
|
// the previous ones (GET without a body).
|
|
18
18
|
async json(path, { signal = null, method = "GET", headers = null, body = null } = {}) {
|
|
19
|
+
if (!apiBase) throw new SdkError("server-unavailable", { why: "no-api-base" });
|
|
19
20
|
if (typeof impl !== "function") throw new SdkError("bad-input", { field: "fetch" });
|
|
20
21
|
const ctrl = new AbortController();
|
|
21
22
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
@@ -47,6 +48,7 @@ export function createHttp({ apiBase, fetchImpl = null, timeoutMs = 20_000 }) {
|
|
|
47
48
|
* refused" are different things, and the caller must see them.
|
|
48
49
|
*/
|
|
49
50
|
async tryJson(path, { signal = null, method = "GET", headers = null, body = null } = {}) {
|
|
51
|
+
if (!apiBase) throw new SdkError("server-unavailable", { why: "no-api-base" });
|
|
50
52
|
if (typeof impl !== "function") throw new SdkError("bad-input", { field: "fetch" });
|
|
51
53
|
const ctrl = new AbortController();
|
|
52
54
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
package/src/index.mjs
CHANGED
|
@@ -142,7 +142,7 @@ const normalizeOptions = (options) => {
|
|
|
142
142
|
return {
|
|
143
143
|
settlement: { chain: settlement.chain, vm: settlement.vm },
|
|
144
144
|
xmrNetwork: o.xmrNetwork,
|
|
145
|
-
apiBase: typeof o.apiBase === "string" && o.apiBase ? o.apiBase :
|
|
145
|
+
apiBase: typeof o.apiBase === "string" && o.apiBase ? o.apiBase : null,
|
|
146
146
|
assetsBase: typeof o.assetsBase === "string" && o.assetsBase ? o.assetsBase : "/",
|
|
147
147
|
mode,
|
|
148
148
|
storage: o.storage || null,
|
package/src/swaps.mjs
CHANGED
|
@@ -46,8 +46,8 @@ import { declineOf, logReaderOf, blockBoundaryOf, DECLINED_TOPIC0, DECLINED_TERM
|
|
|
46
46
|
//
|
|
47
47
|
// WHAT CAN BE A KIND, EXHAUSTIVELY. The record's settlement belongs to the engine (engine/core/swap.js, the only
|
|
48
48
|
// module that assigns swap.settlement), and `declined` is the one kind written by us (src/decline.mjs):
|
|
49
|
-
// `success` - the swap completed and the XMR is on the depositor's address (sweepNow
|
|
50
|
-
// ticker
|
|
49
|
+
// `success` - the swap completed and the XMR is on the depositor's address (sweepNow and the auto-sweep in the
|
|
50
|
+
// ticker; the SDK does not take it from a backend session);
|
|
51
51
|
// `refund_eth` - the ETH went back to the depositor. TWO WRITERS, ONE KIND: the person's own refund (refundEth)
|
|
52
52
|
// and the watchtower inside the ticker both write `kind: "refund_eth"` with the phase
|
|
53
53
|
// `refunded_eth`, so there is exactly ONE refund-shaped kind here and nothing is collapsed;
|
|
@@ -989,20 +989,21 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
989
989
|
}
|
|
990
990
|
}
|
|
991
991
|
|
|
992
|
-
// 5.
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
992
|
+
// 5. A BACKEND IS OPTIONAL AND NEVER A DEFAULT. When the caller configured an API base, the swap is recorded
|
|
993
|
+
// there and the watch row is opened. Without one, the SDK returns the local state and the joint address: the
|
|
994
|
+
// caller watches the chain itself. The owner's rule: no request to the page's own origin.
|
|
995
|
+
let server = null;
|
|
996
|
+
let watch = null;
|
|
997
|
+
if (config && config.apiBase) {
|
|
998
|
+
server = await markOnServer(record.swap, escrowAddress, request.order);
|
|
999
|
+
watch = await openWatch(record.swap, request.order);
|
|
1000
|
+
}
|
|
1000
1001
|
// THE STATE IS NAMED, NOT PASSED OFF AS COMPLETION: the swap reached exactly "waiting for XMR at the joint address",
|
|
1001
|
-
// and there is no arrival yet - `receivedXmr` is
|
|
1002
|
+
// and there is no arrival yet - `receivedXmr` is never implied by zero.
|
|
1002
1003
|
return {
|
|
1003
1004
|
id: record.swap.id || null,
|
|
1004
1005
|
escrow: escrowAddress,
|
|
1005
|
-
address: watch.address,
|
|
1006
|
+
address: (watch && watch.address) || (record.swapWallet && record.swapWallet.address) || null,
|
|
1006
1007
|
step: "xmr-incoming",
|
|
1007
1008
|
server,
|
|
1008
1009
|
watch,
|
|
@@ -1013,8 +1014,9 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
1013
1014
|
/**
|
|
1014
1015
|
* WAIT FOR THE XMR TO ARRIVE AT THE JOINT ADDRESS - the whole point of opening the watch.
|
|
1015
1016
|
*
|
|
1016
|
-
* The
|
|
1017
|
-
* configuration (config.limits.watchPollMs/watchTimeoutMs
|
|
1017
|
+
* The CLIENT checks the arrival from the Monero nodes (`verify`); there is no backend poll by default. The rate
|
|
1018
|
+
* and the timeout come from the configuration (config.limits.watchPollMs/watchTimeoutMs) and are overridable here.
|
|
1019
|
+
* A caller that explicitly configured `apiBase` may use the old watch-row flow instead.
|
|
1018
1020
|
* IT ARRIVED - we return the FACT (the amount, the confirmations, the txid); WE DID NOT WAIT - a refusal with the
|
|
1019
1021
|
* code `xmr-timeout`, separate from the others, so "the wait expired" is confused with neither a breakdown nor an
|
|
1020
1022
|
* empty response. XMR ON THE ADDRESS BUT BARRED BY unlock_time is a THIRD answer with its own code `xmr-locked`
|
|
@@ -1056,6 +1058,11 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
1056
1058
|
: (request.deadline && request.deadline.readyBy !== undefined ? Number(request.deadline.readyBy) : null);
|
|
1057
1059
|
if (readyBy) xmrArrival.assertTimeForConfirmations({ readyBySec: readyBy, nowSec: Math.floor(Date.now() / 1000), minConfirmations, blockTimeSec: limitOf("xmrBlockTimeSec", 120) });
|
|
1058
1060
|
}
|
|
1061
|
+
const hasBackend = Boolean(config && config.apiBase);
|
|
1062
|
+
// THE WATCH DOES NOT CALL A BACKEND BY DEFAULT. The client-side arrival check needs the joint address, the view
|
|
1063
|
+
// secret and the spend public key; without them, and without an explicitly configured backend, there is nothing
|
|
1064
|
+
// to wait on. That is a named refusal, not a request to the page's own origin.
|
|
1065
|
+
if (!verifyRequest && !hasBackend) throw new SdkError("bad-input", { field: "verify", why: "no-backend" });
|
|
1059
1066
|
const started = Date.now();
|
|
1060
1067
|
// --- THE MAKER'S DECLINE: THE WAIT IS NOT STARTED AT ALL (issue #40). -------------------------------------
|
|
1061
1068
|
// A decline leaves the escrow `refunded`, so nothing in the state says "do not wait any longer": without
|
|
@@ -1123,8 +1130,12 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
1123
1130
|
let last = null;
|
|
1124
1131
|
for (;;) {
|
|
1125
1132
|
await readDeclineLogs();
|
|
1126
|
-
|
|
1127
|
-
|
|
1133
|
+
// THE CLIENT-SIDE CHECK DOES NOT ASK THE SERVICE. Only a watch without `verify` and with an explicitly
|
|
1134
|
+
// configured backend reads the watch row.
|
|
1135
|
+
const body = verifyRequest ? {} : await readWatch(id);
|
|
1136
|
+
last = verifyRequest
|
|
1137
|
+
? { address: request.address || escrowAddress, expectedXmr: request.expectedXmr ?? null, status: null }
|
|
1138
|
+
: incomingOf(body);
|
|
1128
1139
|
// THE DECLINE IS DECIDED BEFORE THE ARRIVAL IS LOOKED AT, and before the "foreign response" refusal below:
|
|
1129
1140
|
// a declined swap's answer carries no `receivedXmr` at all, and refusing it as a bad response would name
|
|
1130
1141
|
// the wrong thing (the state is known - it is a decline, not a broken answer).
|
package/src/sweep-restore.mjs
CHANGED
|
@@ -202,15 +202,33 @@ export async function restoreWallet(request = {}) {
|
|
|
202
202
|
}
|
|
203
203
|
windowReport.txs = hashes.length;
|
|
204
204
|
if (hashes.length && typeof driver.scanTxs === "function") {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
try {
|
|
206
|
+
await step("setRestoreHeight", () => driver.setRestoreHeight(tipHeight));
|
|
207
|
+
await step("scanTxs", () => driver.scanTxs(hashes));
|
|
208
|
+
try { await wallet.sync(); } catch { /* the balance below decides */ }
|
|
209
|
+
windowTotal = asAmount(await step("getBalance", () => driver.getBalance()), "balance");
|
|
210
|
+
windowUnlocked = asAmount(await step("unlockedBalance", () => wallet.unlockedBalance()), "unlockedBalance");
|
|
211
|
+
windowReport.used = true;
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// A WINDOW THAT WAS READ BUT NOT APPLIED IS STILL A FAILED WINDOW. `scanTxs` and the balance read use the
|
|
214
|
+
// wallet's own transport; a node that misses one route (`/gettransactions`) or a wallet that refuses the
|
|
215
|
+
// scan must not end the withdrawal. The full scan below is the fallback, and the reason says which step.
|
|
216
|
+
if (e && e.code === "bad-input") throw e;
|
|
217
|
+
windowTotal = 0n;
|
|
218
|
+
windowUnlocked = 0n;
|
|
219
|
+
windowReport.fallback = true;
|
|
220
|
+
windowReport.fallbackWhy = "window-scan";
|
|
221
|
+
windowReport.error = {
|
|
222
|
+
code: e && e.code ? e.code : "wallet-rejected",
|
|
223
|
+
surface: e && e.params && e.params.surface ? e.params.surface : "scanTxs",
|
|
224
|
+
status: null,
|
|
225
|
+
height: null,
|
|
226
|
+
why: e && e.params && e.params.why ? e.params.why : null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
211
229
|
// THE KEY IMAGES OF THE WALLET, CHECKED AT THE NODE. `scanTxs` finds an output; whether the chain already
|
|
212
|
-
// spent it is a separate fact, and the caller must not sign on a guess.
|
|
213
|
-
if (requestWindow.nodeUrl && typeof driver.getKeyImages === "function") {
|
|
230
|
+
// spent it is a separate fact, and the caller must not sign on a guess. A failed scan has no images to ask for.
|
|
231
|
+
if (windowReport.used && requestWindow.nodeUrl && typeof driver.getKeyImages === "function") {
|
|
214
232
|
try {
|
|
215
233
|
const exported = await driver.getKeyImages();
|
|
216
234
|
const images = Array.isArray(exported) ? exported
|
|
@@ -232,13 +250,13 @@ export async function restoreWallet(request = {}) {
|
|
|
232
250
|
} else windowReport.spent = null;
|
|
233
251
|
} else { windowReport.spent = null; windowReport.spentWhy = "no-key-images"; }
|
|
234
252
|
} catch { windowReport.spent = null; windowReport.spentWhy = "check-failed"; }
|
|
235
|
-
} else if (requestWindow.nodeUrl) {
|
|
253
|
+
} else if (windowReport.used && requestWindow.nodeUrl) {
|
|
236
254
|
windowReport.spent = null;
|
|
237
255
|
windowReport.spentWhy = "no-key-images";
|
|
238
256
|
}
|
|
239
257
|
// A WINDOW WITH COINS IS THE ANSWER. A window with none falls through to the full scan unless the caller
|
|
240
258
|
// explicitly turned the fallback off.
|
|
241
|
-
if (windowTotal > 0n || windowUnlocked > 0n || requestWindow.fallback === false) {
|
|
259
|
+
if (windowReport.used && (windowTotal > 0n || windowUnlocked > 0n || requestWindow.fallback === false)) {
|
|
242
260
|
return {
|
|
243
261
|
total: windowTotal.toString(), unlocked: windowUnlocked.toString(), scanHeight, walletHeight: await heightOf(driver, "getHeight"),
|
|
244
262
|
nodeHeight: await heightOf(driver, "getDaemonHeight"), passes: 1, lockedPass: false, window: windowReport,
|
package/src/sweep.mjs
CHANGED
|
@@ -209,30 +209,15 @@ export function createSweep({ http, config, call = null }) {
|
|
|
209
209
|
const spendable = !!wallet.spendHalf;
|
|
210
210
|
if (!spendable) missing.push("spendable");
|
|
211
211
|
|
|
212
|
-
// THE RESTORE HEIGHT
|
|
213
|
-
//
|
|
214
|
-
//
|
|
212
|
+
// THE RESTORE HEIGHT COMES FROM THE FILE OR THE CHAIN. The SDK does not ask a service by address: a static
|
|
213
|
+
// page and a widget must not tell their own host which deal they are recovering. A missing height is named in
|
|
214
|
+
// `missing`; the caller can pass one from `findSwaps` or the person can type it.
|
|
215
215
|
let restoreHeight = Number(wallet.restoreHeight) || 0;
|
|
216
|
-
let row = null;
|
|
217
|
-
try {
|
|
218
|
-
// THE PATH BY NAME, not by a string: the override comes through the `routes` option.
|
|
219
|
-
const answer = await http.json(config.route("swaps"));
|
|
220
|
-
const rows = Array.isArray(answer) ? answer : ((answer && answer.swaps) || []);
|
|
221
|
-
row = rows.find((x) => String((x && x.address) || "") === String(address)) || null;
|
|
222
|
-
} catch { row = null; }
|
|
223
|
-
const fromApp = Number(row && row.restoreHeight) || 0;
|
|
224
|
-
if (fromApp > 0) restoreHeight = fromApp;
|
|
225
216
|
if (!(restoreHeight > 0)) missing.push("restore-height");
|
|
226
217
|
|
|
227
|
-
// THE COUNTERPARTY'S REVEALED HALF - FROM THE CHAIN, VIA THE APP. Exactly as the sweep page does it: POST to
|
|
228
|
-
// the named path, body { network, address }, where address is the ESCROW ADDRESS (not the Monero address).
|
|
229
|
-
// The half is published by the settlement that moves the ETH: a claim or a refund. Before that no one has the
|
|
230
|
-
// spend key, so "not revealed yet" is not an error but a state, and that is exactly where it lands in `missing`.
|
|
231
218
|
// THE COUNTERPARTY'S REVEALED HALF - FROM THE CHAIN WHEN THE CALLER HAS A CHAIN READ. The half is public:
|
|
232
|
-
// `claim` and `refund` store it in `revealedHalf()` and emit `HalfRevealed`.
|
|
233
|
-
//
|
|
234
|
-
// could never withdraw. The app route stays as the fallback for a caller that has no chain read and did
|
|
235
|
-
// configure an API base.
|
|
219
|
+
// `claim` and `refund` store it in `revealedHalf()` and emit `HalfRevealed`. There is no service fallback: a
|
|
220
|
+
// caller without a chain read gets `missing: ["revealed-half"]`, not a request to its own origin.
|
|
236
221
|
let revealed = false;
|
|
237
222
|
let revealedValue = null;
|
|
238
223
|
const escrowAddress = wallet.escrowAddress || (payload.escrow && payload.escrow.address) || null;
|
|
@@ -246,17 +231,6 @@ export function createSweep({ http, config, call = null }) {
|
|
|
246
231
|
}
|
|
247
232
|
} catch { revealed = false; }
|
|
248
233
|
}
|
|
249
|
-
if (!revealed && escrowAddress && config && config.apiBase) {
|
|
250
|
-
try {
|
|
251
|
-
const half = await http.json(config.route("revealedHalf"), {
|
|
252
|
-
method: "POST",
|
|
253
|
-
headers: { "content-type": "application/json" },
|
|
254
|
-
body: JSON.stringify({ network: payload.network, address: escrowAddress }),
|
|
255
|
-
});
|
|
256
|
-
revealed = !!(half && half.ok && half.revealed && half.half);
|
|
257
|
-
if (revealed) revealedValue = String(half.half);
|
|
258
|
-
} catch { revealed = false; }
|
|
259
|
-
}
|
|
260
234
|
if (!revealed) missing.push("revealed-half");
|
|
261
235
|
|
|
262
236
|
return {
|
|
@@ -317,24 +291,6 @@ export function createSweep({ http, config, call = null }) {
|
|
|
317
291
|
// SIGNING AND SENDING ARE DELIBERATELY SPLIT: between them the interface has time to show the person what will
|
|
318
292
|
// leave, rather than telling them about it after sending. An empty remainder stops the withdrawal BEFORE
|
|
319
293
|
// signing - otherwise the wallet would sign a transaction for zero and the refusal would look like success.
|
|
320
|
-
// TELLING THE SERVICE ABOUT THE WITHDRAWAL - SEPARATELY AND REPEATABLY. The mark does not affect the transfer
|
|
321
|
-
// (it is already on the network), so it can be repeated: the interface calls this even after `relay` if the
|
|
322
|
-
// report did not go through then.
|
|
323
|
-
async report(request) {
|
|
324
|
-
if (!request || typeof request.swapId !== "string" || !request.swapId) throw new SdkError("bad-input", { field: "swapId" });
|
|
325
|
-
if (typeof request.txid !== "string" || !request.txid) throw new SdkError("bad-input", { field: "txid" });
|
|
326
|
-
try {
|
|
327
|
-
const answer = await http.json(config.route("swept", { id: request.swapId }), {
|
|
328
|
-
method: "POST",
|
|
329
|
-
headers: { "content-type": "application/json" },
|
|
330
|
-
body: JSON.stringify({ txid: request.txid }),
|
|
331
|
-
});
|
|
332
|
-
return { reported: !(answer && answer.ok === false) };
|
|
333
|
-
} catch {
|
|
334
|
-
// The service is unavailable - that is `reported: false`, not a withdrawal refusal: the money has already left.
|
|
335
|
-
return { reported: false };
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
294
|
|
|
339
295
|
// SIGNING WITHOUT SENDING - AS A SEPARATE STEP. The core HOLDS the signed transaction until the person's
|
|
340
296
|
// decision: otherwise the interface could not show what exactly will leave, and would tell about it only after
|
|
@@ -410,14 +366,9 @@ export function createSweep({ http, config, call = null }) {
|
|
|
410
366
|
try {
|
|
411
367
|
const txids = await wallet.relay(signed);
|
|
412
368
|
const list = Array.isArray(txids) ? txids : [];
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
// here (`report`).
|
|
417
|
-
const reported = plan.swapId && list.length
|
|
418
|
-
? (await api.report({ swapId: plan.swapId, txid: list[0] })).reported
|
|
419
|
-
: false;
|
|
420
|
-
return { txids: list, swept: plan.address, reported };
|
|
369
|
+
// NO SERVICE REPORT. The transfer is on the Monero network; the SDK does not ask the page's own host to
|
|
370
|
+
// record it, so the host cannot link the visitor's IP to the escrow and the withdrawal.
|
|
371
|
+
return { txids: list, swept: plan.address };
|
|
421
372
|
} finally {
|
|
422
373
|
await wallet.close();
|
|
423
374
|
}
|
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.51.
|
|
9
|
+
export const SDK_VERSION = "0.51.2";
|