@lumenwipe/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @lumenwipe/sdk
2
+
3
+ Typed fetch client for the [LumenWipe](https://lumenwipe.com) API. LumenWipe closes Stellar
4
+ accounts non-custodially: it unwinds everything holding an account open, converts leftovers to
5
+ XLM, and merges the account into a destination wallet or exchange.
6
+
7
+ This package only talks to the LumenWipe API over HTTP - it builds no transactions itself and has
8
+ no `@stellar/stellar-sdk` dependency. Reading on-chain state and constructing every unsigned
9
+ transaction happens server-side; this client just relays typed JSON and XDR strings.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @lumenwipe/sdk
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { LumenWipeClient } from "@lumenwipe/sdk";
21
+
22
+ const client = new LumenWipeClient({
23
+ baseUrl: "https://api.lumenwipe.com",
24
+ apiKey: process.env.LUMENWIPE_API_KEY!,
25
+ network: "mainnet", // or "testnet"
26
+ });
27
+
28
+ const account = await client.getAccount(address);
29
+
30
+ const plan = await client.closePlan({
31
+ address,
32
+ destination,
33
+ // ...see @lumenwipe/types for the full request shape
34
+ });
35
+
36
+ const { transactions, remaining } = await client.closeTransactions({
37
+ address,
38
+ destination,
39
+ });
40
+
41
+ // Sign and submit each transaction, then repeat while remaining.requiresAnotherCall is true.
42
+ const result = await client.submit(signedXdr);
43
+ ```
44
+
45
+ For a fully driven close loop (fetch -> verify -> sign -> submit -> repeat), use `runClose`
46
+ instead of calling `closeTransactions`/`submit` by hand:
47
+
48
+ ```ts
49
+ import { runClose } from "@lumenwipe/sdk";
50
+
51
+ await runClose({
52
+ getTransactions: () => client.closeTransactions({ address, destination }),
53
+ verify: (tx) => myVerify(tx), // see "Verification" below - this is your responsibility
54
+ requiredWeight: (tx) => myRequiredWeight(tx),
55
+ sign: (tx, xdr) => mySigner(tx, xdr),
56
+ submit: (tx, xdr) => client.submit(xdr).then((r) => r.hash),
57
+ });
58
+ ```
59
+
60
+ ## Verification is the caller's responsibility
61
+
62
+ This SDK does not verify or sign anything. Every transaction it returns is unsigned XDR built by
63
+ the API from data you provided - before signing, your own code must independently confirm the
64
+ transaction does exactly what the user asked (correct destination, no unexpected operations, no
65
+ added signers, matching amounts) using values from your own inputs, never from the API response
66
+ alone. LumenWipe's own web client's verification logic (`assertCloseIntent` in
67
+ [`apps/web/lib/stellar/verify.ts`](https://github.com/LumenWipe/lumenwipe/blob/main/apps/web/lib/stellar/verify.ts))
68
+ is the reference implementation - read it before wiring up signing in a production integration.
69
+ Never sign a transaction from this SDK without an equivalent check.
70
+
71
+ ## API surface
72
+
73
+ - `health()` - service health check.
74
+ - `getAccount(address, network?)` - current on-chain state for an account.
75
+ - `getPaths(params, network?)` - conversion path quotes for a source asset.
76
+ - `closePlan(body, network?)` - a preview of the full close plan, with blockers if any step
77
+ cannot be closed safely.
78
+ - `closeTransactions(body, network?)` - builds the next round of unsigned transactions.
79
+ - `submit(signedXdr, network?)` - submits a signed transaction.
80
+ - `mediatorCheck(address, network?)` / `mediatorSign(transaction, network?)` - the exchange
81
+ mediator flow (see the architecture doc).
82
+ - `runClose(deps)` - drives the full fetch/verify/sign/submit loop to completion.
83
+
84
+ Request and response types come from `@lumenwipe/types` and are re-exported from this package, so
85
+ no separate install is needed.
86
+
87
+ ## Errors
88
+
89
+ - `LumenWipeApiError` - thrown on any non-2xx API response; carries `status` and the parsed
90
+ error `body`.
91
+ - `LumenWipeTimeoutError` - thrown when a request exceeds the configured `timeout`
92
+ (default 30s).
93
+
94
+ ## Links
95
+
96
+ - [Architecture](https://github.com/LumenWipe/lumenwipe/blob/main/docs/architecture.md)
97
+ - [Full documentation](https://docs.lumenwipe.com)
98
+ - [Issues](https://github.com/LumenWipe/lumenwipe/issues)
99
+
100
+ ## License
101
+
102
+ Apache-2.0
package/dist/index.cjs ADDED
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ InsufficientSignatureWeightError: () => InsufficientSignatureWeightError,
24
+ LumenWipeApiError: () => LumenWipeApiError,
25
+ LumenWipeClient: () => LumenWipeClient,
26
+ LumenWipeTimeoutError: () => LumenWipeTimeoutError,
27
+ runClose: () => runClose
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/errors.ts
32
+ var LumenWipeApiError = class extends Error {
33
+ constructor(status, body) {
34
+ super(`LumenWipe API error ${status}`);
35
+ this.status = status;
36
+ this.body = body;
37
+ this.name = "LumenWipeApiError";
38
+ }
39
+ };
40
+ var LumenWipeTimeoutError = class extends Error {
41
+ constructor(timeoutMs) {
42
+ super(`LumenWipe API request timed out after ${timeoutMs}ms`);
43
+ this.timeoutMs = timeoutMs;
44
+ this.name = "LumenWipeTimeoutError";
45
+ }
46
+ };
47
+
48
+ // src/http.ts
49
+ var HttpTransport = class {
50
+ constructor(baseUrl, apiKey, timeout, doFetch) {
51
+ this.baseUrl = baseUrl;
52
+ this.apiKey = apiKey;
53
+ this.timeout = timeout;
54
+ this.doFetch = doFetch;
55
+ }
56
+ async request(method, path, body) {
57
+ const headers = { Authorization: `Bearer ${this.apiKey}` };
58
+ if (body !== void 0) headers["Content-Type"] = "application/json";
59
+ const controller = new AbortController();
60
+ const bounded = Number.isFinite(this.timeout) && this.timeout > 0;
61
+ const timer = bounded ? setTimeout(() => controller.abort(), this.timeout) : void 0;
62
+ try {
63
+ const res = await this.doFetch(`${this.baseUrl}${path}`, {
64
+ method,
65
+ headers,
66
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
67
+ signal: controller.signal
68
+ });
69
+ const text = await res.text();
70
+ let parsed = void 0;
71
+ if (text) {
72
+ try {
73
+ parsed = JSON.parse(text);
74
+ } catch (e) {
75
+ parsed = text;
76
+ }
77
+ }
78
+ if (!res.ok) throw new LumenWipeApiError(res.status, parsed);
79
+ return parsed;
80
+ } catch (e) {
81
+ if (controller.signal.aborted) throw new LumenWipeTimeoutError(this.timeout);
82
+ throw e;
83
+ } finally {
84
+ if (timer) clearTimeout(timer);
85
+ }
86
+ }
87
+ };
88
+
89
+ // src/client.ts
90
+ var LumenWipeClient = class {
91
+ constructor(options) {
92
+ var _a, _b, _c;
93
+ const resolved = (_a = options.fetch) != null ? _a : globalThis.fetch;
94
+ if (!resolved) {
95
+ throw new Error("No fetch implementation available; pass one via options.fetch.");
96
+ }
97
+ this.http = new HttpTransport(
98
+ options.baseUrl.replace(/\/+$/, ""),
99
+ options.apiKey,
100
+ (_b = options.timeout) != null ? _b : 3e4,
101
+ resolved
102
+ );
103
+ this.defaultNetwork = (_c = options.network) != null ? _c : "testnet";
104
+ }
105
+ health() {
106
+ return this.http.request("GET", "/health");
107
+ }
108
+ getAccount(address, network = this.defaultNetwork) {
109
+ return this.http.request(
110
+ "GET",
111
+ `/${network}/account/${encodeURIComponent(address)}`
112
+ );
113
+ }
114
+ getPaths(params, network = this.defaultNetwork) {
115
+ const query = new URLSearchParams({ fromAsset: params.fromAsset, amount: params.amount });
116
+ return this.http.request("GET", `/${network}/paths?${query.toString()}`);
117
+ }
118
+ closePlan(body, network = this.defaultNetwork) {
119
+ return this.http.request("POST", `/v1/${network}/close/plan`, body);
120
+ }
121
+ /**
122
+ * Builds the next unsigned transaction(s) for a close. A close can span several
123
+ * transactions (a fused close, or separate claim / cleanup / mediator-merge steps),
124
+ * so the response's `remaining.requiresAnotherCall` says whether more follow: sign and
125
+ * submit the returned transactions in `order`, wait for confirmation, then call this
126
+ * again until `requiresAnotherCall` is false.
127
+ */
128
+ closeTransactions(body, network = this.defaultNetwork) {
129
+ return this.http.request(
130
+ "POST",
131
+ `/v1/${network}/close/transactions`,
132
+ body
133
+ );
134
+ }
135
+ submit(signedXdr, network = this.defaultNetwork) {
136
+ return this.http.request("POST", `/v1/${network}/submit`, { signedXdr });
137
+ }
138
+ mediatorCheck(address, network = this.defaultNetwork) {
139
+ return this.http.request(
140
+ "GET",
141
+ `/${network}/mediator/check/${encodeURIComponent(address)}`
142
+ );
143
+ }
144
+ mediatorSign(transaction, network = this.defaultNetwork) {
145
+ return this.http.request("POST", `/${network}/mediator/sign`, {
146
+ transaction
147
+ });
148
+ }
149
+ };
150
+
151
+ // src/close-engine.ts
152
+ var InsufficientSignatureWeightError = class extends Error {
153
+ constructor(pending) {
154
+ super(
155
+ `Transaction ${pending.tx.id} needs signing weight ${pending.requiredWeight} but only has ${pending.accumulatedWeight}.`
156
+ );
157
+ this.pending = pending;
158
+ this.name = "InsufficientSignatureWeightError";
159
+ }
160
+ };
161
+ async function runClose(deps, resume) {
162
+ var _a, _b;
163
+ const maxRounds = (_a = deps.maxRounds) != null ? _a : 25;
164
+ if (resume) {
165
+ await signOrThrow(deps, resume.tx, resume.xdr, resume.queue, resume.requiresAnotherCall);
166
+ await processTxs(deps, resume.queue, resume.requiresAnotherCall);
167
+ if (!resume.requiresAnotherCall) return;
168
+ }
169
+ for (let round = 0; round < maxRounds; round++) {
170
+ (_b = deps.onProgress) == null ? void 0 : _b.call(deps, "Preparing transactions\u2026");
171
+ const batch = await deps.getTransactions();
172
+ const txs = [...batch.transactions].sort((a, b) => a.order - b.order);
173
+ await processTxs(deps, txs, batch.remaining.requiresAnotherCall);
174
+ if (!batch.remaining.requiresAnotherCall) return;
175
+ }
176
+ throw new Error("The close did not converge after the maximum number of rounds.");
177
+ }
178
+ async function processTxs(deps, txs, requiresAnotherCall) {
179
+ for (let i = 0; i < txs.length; i++) {
180
+ const tx = txs[i];
181
+ await deps.verify(tx);
182
+ await signOrThrow(deps, tx, tx.xdr, txs.slice(i + 1), requiresAnotherCall);
183
+ }
184
+ }
185
+ async function signOrThrow(deps, tx, xdr, queue, requiresAnotherCall) {
186
+ var _a;
187
+ const required = deps.requiredWeight(tx);
188
+ const { xdr: signedXdr, weight } = await deps.sign(tx, xdr);
189
+ if (weight < required) {
190
+ throw new InsufficientSignatureWeightError({
191
+ tx,
192
+ xdr: signedXdr,
193
+ requiredWeight: required,
194
+ accumulatedWeight: weight,
195
+ queue,
196
+ requiresAnotherCall
197
+ });
198
+ }
199
+ const hash = await deps.submit(tx, signedXdr);
200
+ (_a = deps.onConfirmed) == null ? void 0 : _a.call(deps, tx, hash);
201
+ }
202
+ // Annotate the CommonJS export names for ESM import in node:
203
+ 0 && (module.exports = {
204
+ InsufficientSignatureWeightError,
205
+ LumenWipeApiError,
206
+ LumenWipeClient,
207
+ LumenWipeTimeoutError,
208
+ runClose
209
+ });