@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 +102 -0
- package/dist/index.cjs +209 -0
- package/dist/index.d.cts +577 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +178 -0
- package/package.json +59 -0
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
|
+
});
|