neutron-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/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/index.cjs +883 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +662 -0
- package/dist/index.d.ts +662 -0
- package/dist/index.js +824 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var NeutronError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "NeutronError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var NeutronApiError = class extends NeutronError {
|
|
12
|
+
/** HTTP status code */
|
|
13
|
+
status;
|
|
14
|
+
/** Neutron error code (e.g. "2005") */
|
|
15
|
+
code;
|
|
16
|
+
/** Raw error body from the API */
|
|
17
|
+
body;
|
|
18
|
+
constructor(status, body) {
|
|
19
|
+
const message = body?.error || body?.message || `API error ${status}`;
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "NeutronApiError";
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.code = body?.code;
|
|
24
|
+
this.body = body;
|
|
25
|
+
}
|
|
26
|
+
/** True if this is a rate limit error (429) */
|
|
27
|
+
get isRateLimited() {
|
|
28
|
+
return this.status === 429;
|
|
29
|
+
}
|
|
30
|
+
/** True if this is an auth error (401/403) */
|
|
31
|
+
get isAuthError() {
|
|
32
|
+
return this.status === 401 || this.status === 403;
|
|
33
|
+
}
|
|
34
|
+
/** True if retrying might help (5xx, 429) */
|
|
35
|
+
get isRetryable() {
|
|
36
|
+
return this.status === 429 || this.status >= 500;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var NeutronAuthError = class extends NeutronError {
|
|
40
|
+
constructor(message = "Authentication failed. Check your API key and secret.") {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "NeutronAuthError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var NeutronTimeoutError = class extends NeutronError {
|
|
46
|
+
constructor(timeoutMs) {
|
|
47
|
+
super(`Request timed out after ${timeoutMs}ms`);
|
|
48
|
+
this.name = "NeutronTimeoutError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var NeutronValidationError = class extends NeutronError {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "NeutronValidationError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/client.ts
|
|
59
|
+
var DEFAULT_BASE_URL = "https://enapi.npay.live";
|
|
60
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
61
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
62
|
+
var TOKEN_REFRESH_BUFFER_MS = 6e4;
|
|
63
|
+
var HttpClient = class {
|
|
64
|
+
apiKey;
|
|
65
|
+
apiSecret;
|
|
66
|
+
baseUrl;
|
|
67
|
+
timeout;
|
|
68
|
+
maxRetries;
|
|
69
|
+
debug;
|
|
70
|
+
accessToken = null;
|
|
71
|
+
accountId = null;
|
|
72
|
+
tokenExpiry = 0;
|
|
73
|
+
constructor(config) {
|
|
74
|
+
if (!config.apiKey) throw new NeutronAuthError("apiKey is required");
|
|
75
|
+
if (!config.apiSecret) throw new NeutronAuthError("apiSecret is required");
|
|
76
|
+
this.apiKey = config.apiKey;
|
|
77
|
+
this.apiSecret = config.apiSecret;
|
|
78
|
+
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
79
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
80
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
81
|
+
this.debug = config.debug ?? false;
|
|
82
|
+
}
|
|
83
|
+
log(message, data) {
|
|
84
|
+
if (!this.debug) return;
|
|
85
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
86
|
+
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
87
|
+
console.error(`[neutron-sdk ${ts}] ${message}${extra}`);
|
|
88
|
+
}
|
|
89
|
+
// ── Auth ──────────────────────────────────────────────
|
|
90
|
+
generateSignature(payload) {
|
|
91
|
+
const stringToSign = `${this.apiKey}&payload=${payload}`;
|
|
92
|
+
return crypto.createHmac("sha256", this.apiSecret).update(stringToSign).digest("hex");
|
|
93
|
+
}
|
|
94
|
+
get isTokenValid() {
|
|
95
|
+
return !!(this.accessToken && this.accountId && Date.now() < this.tokenExpiry - TOKEN_REFRESH_BUFFER_MS);
|
|
96
|
+
}
|
|
97
|
+
async authenticate() {
|
|
98
|
+
const payload = JSON.stringify({ test: "auth" });
|
|
99
|
+
const signature = this.generateSignature(payload);
|
|
100
|
+
const response = await this.rawFetch(
|
|
101
|
+
`${this.baseUrl}/api/v2/authentication/token-signature`,
|
|
102
|
+
{
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
"X-Api-Key": this.apiKey,
|
|
107
|
+
"X-Api-Signature": signature
|
|
108
|
+
},
|
|
109
|
+
body: payload
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const body = await response.json().catch(() => ({}));
|
|
114
|
+
throw new NeutronAuthError(
|
|
115
|
+
body.error || body.message || `Authentication failed (${response.status})`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const raw = await response.json();
|
|
119
|
+
const result = raw.data ?? raw;
|
|
120
|
+
this.accountId = result.accountId;
|
|
121
|
+
this.accessToken = result.accessToken;
|
|
122
|
+
this.tokenExpiry = typeof result.expiredAt === "number" ? result.expiredAt : new Date(result.expiredAt).getTime();
|
|
123
|
+
return result;
|
|
124
|
+
this.log("Authenticated", { accountId: this.accountId });
|
|
125
|
+
}
|
|
126
|
+
async ensureAuth() {
|
|
127
|
+
if (!this.isTokenValid) {
|
|
128
|
+
await this.authenticate();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
getAccountId() {
|
|
132
|
+
if (!this.accountId) {
|
|
133
|
+
throw new NeutronAuthError("Not authenticated. Call a method first or use neutron.account.get().");
|
|
134
|
+
}
|
|
135
|
+
return this.accountId;
|
|
136
|
+
}
|
|
137
|
+
async ensureAuthAndGetAccountId() {
|
|
138
|
+
await this.ensureAuth();
|
|
139
|
+
return this.accountId;
|
|
140
|
+
}
|
|
141
|
+
// ── HTTP ──────────────────────────────────────────────
|
|
142
|
+
async rawFetch(url, init) {
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
145
|
+
try {
|
|
146
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err.name === "AbortError") {
|
|
149
|
+
throw new NeutronTimeoutError(this.timeout);
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async request(method, path, body) {
|
|
157
|
+
await this.ensureAuth();
|
|
158
|
+
let lastError;
|
|
159
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
160
|
+
if (attempt > 0) {
|
|
161
|
+
await new Promise((r) => setTimeout(r, Math.pow(2, attempt - 1) * 1e3));
|
|
162
|
+
if (!this.isTokenValid) await this.authenticate();
|
|
163
|
+
}
|
|
164
|
+
const url = `${this.baseUrl}${path}`;
|
|
165
|
+
this.log(`${method} ${path}`);
|
|
166
|
+
const headers = {
|
|
167
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
168
|
+
};
|
|
169
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
170
|
+
const response = await this.rawFetch(url, {
|
|
171
|
+
method,
|
|
172
|
+
headers,
|
|
173
|
+
body: body ? JSON.stringify(body) : void 0
|
|
174
|
+
});
|
|
175
|
+
if (response.ok) {
|
|
176
|
+
return await response.json();
|
|
177
|
+
}
|
|
178
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
179
|
+
const apiError = new NeutronApiError(response.status, errorBody);
|
|
180
|
+
if (response.status === 401 && attempt < this.maxRetries) {
|
|
181
|
+
this.accessToken = null;
|
|
182
|
+
lastError = apiError;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (apiError.isRetryable && attempt < this.maxRetries) {
|
|
186
|
+
lastError = apiError;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
throw apiError;
|
|
190
|
+
}
|
|
191
|
+
throw lastError;
|
|
192
|
+
}
|
|
193
|
+
async get(path) {
|
|
194
|
+
return this.request("GET", path);
|
|
195
|
+
}
|
|
196
|
+
async post(path, body) {
|
|
197
|
+
return this.request("POST", path, body);
|
|
198
|
+
}
|
|
199
|
+
async put(path, body) {
|
|
200
|
+
return this.request("PUT", path, body);
|
|
201
|
+
}
|
|
202
|
+
async del(path) {
|
|
203
|
+
return this.request("DELETE", path);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/sanitize.ts
|
|
208
|
+
function sanitizePathParam(value, name) {
|
|
209
|
+
if (!value || typeof value !== "string") {
|
|
210
|
+
throw new NeutronValidationError(`${name} is required and must be a non-empty string.`);
|
|
211
|
+
}
|
|
212
|
+
const sanitized = value.replace(/[^a-zA-Z0-9\-_.]/g, "");
|
|
213
|
+
if (sanitized !== value) {
|
|
214
|
+
throw new NeutronValidationError(
|
|
215
|
+
`${name} contains invalid characters. Only alphanumeric, hyphens, underscores, and dots are allowed.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (sanitized.includes("..")) {
|
|
219
|
+
throw new NeutronValidationError(`${name} cannot contain path traversal sequences.`);
|
|
220
|
+
}
|
|
221
|
+
return sanitized;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/resources/account.ts
|
|
225
|
+
var AccountResource = class {
|
|
226
|
+
constructor(client) {
|
|
227
|
+
this.client = client;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get account info: display name, status, country, timezone.
|
|
231
|
+
*/
|
|
232
|
+
async get() {
|
|
233
|
+
const accountId = await this.client.ensureAuthAndGetAccountId();
|
|
234
|
+
const res = await this.client.get(`/api/v2/account/${accountId}`);
|
|
235
|
+
return res.data ?? res;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* List all wallets with balances (BTC, USDT, fiat).
|
|
239
|
+
*/
|
|
240
|
+
async wallets() {
|
|
241
|
+
const accountId = await this.client.ensureAuthAndGetAccountId();
|
|
242
|
+
const res = await this.client.get(`/api/v2/account/${accountId}/wallet/`);
|
|
243
|
+
return res.data ?? res;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get a specific wallet by ID.
|
|
247
|
+
*/
|
|
248
|
+
async wallet(walletId) {
|
|
249
|
+
sanitizePathParam(walletId, "walletId");
|
|
250
|
+
const accountId = await this.client.ensureAuthAndGetAccountId();
|
|
251
|
+
const res = await this.client.get(`/api/v2/account/${accountId}/wallet/${walletId}`);
|
|
252
|
+
return res.data ?? res;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get your Bitcoin on-chain deposit address (static, reusable).
|
|
256
|
+
*/
|
|
257
|
+
async btcAddress() {
|
|
258
|
+
const raw = await this.client.get(`/api/v2/account/onchain-address`);
|
|
259
|
+
const data = raw?.data ?? raw;
|
|
260
|
+
return { address: data.staticOnchainAddress };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get your USDT deposit address.
|
|
264
|
+
* @param chain "TRON" (default, recommended) or "ETH"
|
|
265
|
+
*/
|
|
266
|
+
async usdtAddress(chain = "TRON") {
|
|
267
|
+
const raw = await this.client.get(
|
|
268
|
+
`/api/v2/account/stablecoin-onchain-address?walletCcy=USDT&chainId=${chain}`
|
|
269
|
+
);
|
|
270
|
+
const data = raw?.data ?? raw;
|
|
271
|
+
return {
|
|
272
|
+
address: data.staticStablecoinOnchainAddress || data.staticOnchainAddress,
|
|
273
|
+
chain
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/resources/transactions.ts
|
|
279
|
+
var TransactionsResource = class {
|
|
280
|
+
constructor(client) {
|
|
281
|
+
this.client = client;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Create a transaction (returns a quote). Call `.confirm()` to execute.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* // Lightning receive (create invoice)
|
|
288
|
+
* const txn = await neutron.transactions.create({
|
|
289
|
+
* sourceReq: { ccy: "BTC", method: "lightning", reqDetails: {} },
|
|
290
|
+
* destReq: { ccy: "BTC", method: "neutronpay", amtRequested: 0.0001, reqDetails: {} },
|
|
291
|
+
* });
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* // Lightning send (pay invoice)
|
|
295
|
+
* const txn = await neutron.transactions.create({
|
|
296
|
+
* sourceReq: { ccy: "BTC", method: "neutronpay" },
|
|
297
|
+
* destReq: { ccy: "BTC", method: "lightning", reqDetails: { paymentRequest: "lnbc..." } },
|
|
298
|
+
* });
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* // Internal swap (BTC → USDT)
|
|
302
|
+
* const txn = await neutron.transactions.create({
|
|
303
|
+
* sourceReq: { ccy: "BTC", method: "neutronpay", amtRequested: 0.001, reqDetails: {} },
|
|
304
|
+
* destReq: { ccy: "USDT", method: "neutronpay", reqDetails: {} },
|
|
305
|
+
* });
|
|
306
|
+
*/
|
|
307
|
+
async create(params) {
|
|
308
|
+
return this.client.post(`/api/v2/transaction`, params);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Confirm a quoted transaction to execute it.
|
|
312
|
+
* After confirmation, Lightning invoices become payable and sends are dispatched.
|
|
313
|
+
*/
|
|
314
|
+
async confirm(txnId) {
|
|
315
|
+
sanitizePathParam(txnId, "txnId");
|
|
316
|
+
return this.client.put(`/api/v2/transaction/${txnId}/confirm`);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get transaction status and details.
|
|
320
|
+
*/
|
|
321
|
+
async get(txnId) {
|
|
322
|
+
sanitizePathParam(txnId, "txnId");
|
|
323
|
+
return this.client.get(`/api/v2/transaction/${txnId}`);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* List transactions with optional filters.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* const completed = await neutron.transactions.list({ status: "completed", limit: 10 });
|
|
330
|
+
*/
|
|
331
|
+
async list(params) {
|
|
332
|
+
const qs = new URLSearchParams();
|
|
333
|
+
if (params) {
|
|
334
|
+
for (const [k, v] of Object.entries(params)) {
|
|
335
|
+
if (v !== void 0 && v !== null) qs.append(k, String(v));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const query = qs.toString();
|
|
339
|
+
const res = await this.client.get(`/api/v2/transaction${query ? `?${query}` : ""}`);
|
|
340
|
+
return res.data ?? res;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Cancel a quoted (unconfirmed) transaction.
|
|
344
|
+
*/
|
|
345
|
+
async cancel(txnId) {
|
|
346
|
+
sanitizePathParam(txnId, "txnId");
|
|
347
|
+
return this.client.put(`/api/v2/transaction/${txnId}/cancel`);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Wait for a transaction to reach a final state. Polls at the given interval.
|
|
351
|
+
*
|
|
352
|
+
* @param txnId Transaction ID
|
|
353
|
+
* @param options.intervalMs Polling interval in ms (default: 3000)
|
|
354
|
+
* @param options.timeoutMs Max wait time in ms (default: 300000 = 5 min)
|
|
355
|
+
* @param options.onStateChange Callback fired on each state change
|
|
356
|
+
* @returns The transaction in a final state
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* const txn = await neutron.transactions.waitForCompletion(txnId, {
|
|
360
|
+
* onStateChange: (state) => console.log("State:", state),
|
|
361
|
+
* });
|
|
362
|
+
*/
|
|
363
|
+
async waitForCompletion(txnId, options) {
|
|
364
|
+
const FINAL_STATES = ["completed", "failed", "expired", "rejected", "error", "usercanceled"];
|
|
365
|
+
const interval = options?.intervalMs ?? 3e3;
|
|
366
|
+
const timeout = options?.timeoutMs ?? 3e5;
|
|
367
|
+
const start = Date.now();
|
|
368
|
+
let lastState = "";
|
|
369
|
+
while (Date.now() - start < timeout) {
|
|
370
|
+
const txn = await this.get(txnId);
|
|
371
|
+
if (txn.txnState !== lastState) {
|
|
372
|
+
lastState = txn.txnState;
|
|
373
|
+
options?.onStateChange?.(txn.txnState, txn);
|
|
374
|
+
}
|
|
375
|
+
if (FINAL_STATES.includes(txn.txnState)) {
|
|
376
|
+
return txn;
|
|
377
|
+
}
|
|
378
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`Transaction ${txnId} did not complete within ${timeout}ms`);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/resources/lightning.ts
|
|
385
|
+
var LightningResource = class {
|
|
386
|
+
constructor(client) {
|
|
387
|
+
this.client = client;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Create a Lightning invoice to receive Bitcoin. Auto-confirms — ready to pay immediately.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* const invoice = await neutron.lightning.createInvoice({ amountSats: 10000 });
|
|
394
|
+
* console.log(invoice.invoice); // "lnbc100u1p..."
|
|
395
|
+
* console.log(invoice.qrPageUrl); // hosted QR code page
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* const invoice = await neutron.lightning.createInvoice({
|
|
399
|
+
* amountBtc: 0.001,
|
|
400
|
+
* memo: "Order #1234",
|
|
401
|
+
* extRefId: "order-1234",
|
|
402
|
+
* });
|
|
403
|
+
*/
|
|
404
|
+
async createInvoice(params) {
|
|
405
|
+
let btcAmount;
|
|
406
|
+
if (params.amountSats !== void 0 && params.amountBtc !== void 0) {
|
|
407
|
+
throw new NeutronValidationError("Provide either amountSats or amountBtc, not both.");
|
|
408
|
+
}
|
|
409
|
+
if (params.amountSats !== void 0) {
|
|
410
|
+
if (params.amountSats <= 0) throw new NeutronValidationError("amountSats must be positive.");
|
|
411
|
+
btcAmount = params.amountSats / 1e8;
|
|
412
|
+
} else if (params.amountBtc !== void 0) {
|
|
413
|
+
if (params.amountBtc <= 0) throw new NeutronValidationError("amountBtc must be positive.");
|
|
414
|
+
btcAmount = params.amountBtc;
|
|
415
|
+
} else {
|
|
416
|
+
throw new NeutronValidationError("Provide either amountSats or amountBtc.");
|
|
417
|
+
}
|
|
418
|
+
const txn = await this.client.post(`/api/v2/transaction`, {
|
|
419
|
+
extRefId: params.extRefId,
|
|
420
|
+
sourceReq: { ccy: "BTC", method: "lightning", reqDetails: {} },
|
|
421
|
+
destReq: {
|
|
422
|
+
ccy: "BTC",
|
|
423
|
+
method: "neutronpay",
|
|
424
|
+
amtRequested: btcAmount,
|
|
425
|
+
reqDetails: {}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
const confirmed = await this.client.put(
|
|
429
|
+
`/api/v2/transaction/${txn.txnId}/confirm`
|
|
430
|
+
);
|
|
431
|
+
return {
|
|
432
|
+
txnId: confirmed.txnId,
|
|
433
|
+
invoice: confirmed.sourceReq?.reqDetails?.paymentRequest ?? "",
|
|
434
|
+
qrPageUrl: confirmed.sourceReq?.reqDetails?.invoicePageUrl,
|
|
435
|
+
amountBtc: btcAmount,
|
|
436
|
+
amountSats: Math.round(btcAmount * 1e8),
|
|
437
|
+
status: confirmed.txnState
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Pay a Lightning invoice (BOLT11).
|
|
442
|
+
* Returns a quoted transaction — call `neutron.transactions.confirm()` to send.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* const txn = await neutron.lightning.payInvoice("lnbc100u1p...");
|
|
446
|
+
* // Review fees: txn.sourceReq.neutronpayFees
|
|
447
|
+
* await neutron.transactions.confirm(txn.txnId);
|
|
448
|
+
*/
|
|
449
|
+
async payInvoice(invoice, extRefId) {
|
|
450
|
+
return this.client.post(`/api/v2/transaction`, {
|
|
451
|
+
extRefId,
|
|
452
|
+
sourceReq: { ccy: "BTC", method: "neutronpay" },
|
|
453
|
+
destReq: {
|
|
454
|
+
ccy: "BTC",
|
|
455
|
+
method: "lightning",
|
|
456
|
+
reqDetails: { paymentRequest: invoice }
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Send to a Lightning Address (user@domain.com).
|
|
462
|
+
* Returns a quoted transaction — call `neutron.transactions.confirm()` to send.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* const txn = await neutron.lightning.payAddress("alice@getalby.com", { amountSats: 5000 });
|
|
466
|
+
* await neutron.transactions.confirm(txn.txnId);
|
|
467
|
+
*/
|
|
468
|
+
async payAddress(address, params) {
|
|
469
|
+
let btcAmount;
|
|
470
|
+
if (params.amountSats !== void 0) {
|
|
471
|
+
btcAmount = params.amountSats / 1e8;
|
|
472
|
+
} else if (params.amountBtc !== void 0) {
|
|
473
|
+
btcAmount = params.amountBtc;
|
|
474
|
+
} else {
|
|
475
|
+
throw new NeutronValidationError("Provide either amountSats or amountBtc.");
|
|
476
|
+
}
|
|
477
|
+
return this.client.post(`/api/v2/transaction`, {
|
|
478
|
+
extRefId: params.extRefId,
|
|
479
|
+
sourceReq: { ccy: "BTC", method: "neutronpay", amtRequested: btcAmount },
|
|
480
|
+
destReq: {
|
|
481
|
+
ccy: "BTC",
|
|
482
|
+
method: "lnurl",
|
|
483
|
+
reqDetails: { lnurl: address }
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Decode a BOLT11 invoice to inspect it before paying.
|
|
489
|
+
* Returns amount, expiry, destination node, description, and payment status.
|
|
490
|
+
*/
|
|
491
|
+
async decodeInvoice(invoice) {
|
|
492
|
+
return this.client.get(
|
|
493
|
+
`/api/v2/lightning/invoice?invoice=${encodeURIComponent(invoice)}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Resolve a Lightning Address (user@domain.com) to check if it exists
|
|
498
|
+
* and get min/max sendable amounts.
|
|
499
|
+
*
|
|
500
|
+
* @param amountMsat Optional: get a specific invoice for this amount (in millisatoshis)
|
|
501
|
+
*/
|
|
502
|
+
async resolveAddress(address, amountMsat) {
|
|
503
|
+
let path = `/api/v2/lightning/resolve-ln-address?lnAddress=${encodeURIComponent(address)}`;
|
|
504
|
+
if (amountMsat !== void 0) path += `&amount=${amountMsat}`;
|
|
505
|
+
return this.client.get(path);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Resolve an LNURL string to see its type (pay/withdraw/channel) and parameters.
|
|
509
|
+
*/
|
|
510
|
+
async resolveLnurl(lnurl) {
|
|
511
|
+
return this.client.get(
|
|
512
|
+
`/api/v2/lightning/resolve-lnurl?lnurl=${encodeURIComponent(lnurl)}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// src/resources/webhooks.ts
|
|
518
|
+
import crypto2 from "crypto";
|
|
519
|
+
var WebhooksResource = class {
|
|
520
|
+
constructor(client) {
|
|
521
|
+
this.client = client;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Register a webhook to receive transaction state change notifications.
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* const webhook = await neutron.webhooks.create({
|
|
528
|
+
* callback: "https://myapp.com/webhooks/neutron",
|
|
529
|
+
* secret: "my-webhook-secret",
|
|
530
|
+
* });
|
|
531
|
+
*/
|
|
532
|
+
async create(params) {
|
|
533
|
+
return this.client.post(`/api/v2/webhook`, params);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* List all registered webhooks.
|
|
537
|
+
*/
|
|
538
|
+
async list() {
|
|
539
|
+
const res = await this.client.get(`/api/v2/webhook`);
|
|
540
|
+
return res.data ?? res;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Update a webhook's callback URL or secret.
|
|
544
|
+
*/
|
|
545
|
+
async update(webhookId, params) {
|
|
546
|
+
sanitizePathParam(webhookId, "webhookId");
|
|
547
|
+
return this.client.put(`/api/v2/webhook/${webhookId}`, params);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Delete a webhook.
|
|
551
|
+
*/
|
|
552
|
+
async delete(webhookId) {
|
|
553
|
+
sanitizePathParam(webhookId, "webhookId");
|
|
554
|
+
await this.client.del(`/api/v2/webhook/${webhookId}`);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Verify a webhook signature from an incoming request.
|
|
558
|
+
* Throws if the signature is invalid.
|
|
559
|
+
*
|
|
560
|
+
* @param body The raw request body (string or Buffer)
|
|
561
|
+
* @param signature The `X-Neutronpay-Signature` header value
|
|
562
|
+
* @param secret Your webhook secret
|
|
563
|
+
* @returns The parsed event payload
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* // Express
|
|
567
|
+
* app.post("/webhooks/neutron", express.raw({ type: "application/json" }), (req, res) => {
|
|
568
|
+
* try {
|
|
569
|
+
* const event = Neutron.webhooks.verifySignature(
|
|
570
|
+
* req.body,
|
|
571
|
+
* req.headers["x-neutronpay-signature"],
|
|
572
|
+
* "my-webhook-secret"
|
|
573
|
+
* );
|
|
574
|
+
* // event is verified and safe to use
|
|
575
|
+
* if (event.txnState === "completed") fulfillOrder(event.extRefId);
|
|
576
|
+
* } catch (err) {
|
|
577
|
+
* return res.status(401).send("Invalid signature");
|
|
578
|
+
* }
|
|
579
|
+
* res.status(200).send("OK");
|
|
580
|
+
* });
|
|
581
|
+
*/
|
|
582
|
+
static verifySignature(body, signature, secret) {
|
|
583
|
+
if (!signature) {
|
|
584
|
+
throw new NeutronValidationError("Missing webhook signature header (X-Neutronpay-Signature)");
|
|
585
|
+
}
|
|
586
|
+
if (!secret) {
|
|
587
|
+
throw new NeutronValidationError("Webhook secret is required for verification");
|
|
588
|
+
}
|
|
589
|
+
const bodyStr = typeof body === "string" ? body : body.toString("utf8");
|
|
590
|
+
const expected = crypto2.createHmac("sha256", secret).update(bodyStr).digest("hex");
|
|
591
|
+
const sigBuf = Buffer.from(signature);
|
|
592
|
+
const expBuf = Buffer.from(expected);
|
|
593
|
+
const isValid = sigBuf.length === expBuf.length && crypto2.timingSafeEqual(sigBuf, expBuf);
|
|
594
|
+
if (!isValid) {
|
|
595
|
+
throw new NeutronValidationError("Invalid webhook signature");
|
|
596
|
+
}
|
|
597
|
+
return JSON.parse(bodyStr);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/resources/rates.ts
|
|
602
|
+
var RatesResource = class {
|
|
603
|
+
constructor(client) {
|
|
604
|
+
this.client = client;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Get current BTC exchange rates against all supported currencies.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* const rates = await neutron.rates.get();
|
|
611
|
+
* console.log(rates); // { BTCUSD: 97500, BTCVND: 2437500000, ... }
|
|
612
|
+
*/
|
|
613
|
+
async get() {
|
|
614
|
+
const res = await this.client.get(`/api/v2/rate`);
|
|
615
|
+
return res.data ?? res;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/resources/fiat.ts
|
|
620
|
+
var FiatResource = class {
|
|
621
|
+
constructor(client) {
|
|
622
|
+
this.client = client;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* List banks and financial institutions for a country.
|
|
626
|
+
* Returns institution codes needed for fiat payouts.
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* const banks = await neutron.fiat.institutions("VN");
|
|
630
|
+
* // [{ code: "970422", name: "MB Bank" }, ...]
|
|
631
|
+
*/
|
|
632
|
+
async institutions(countryCode) {
|
|
633
|
+
sanitizePathParam(countryCode, "countryCode");
|
|
634
|
+
const res = await this.client.get(
|
|
635
|
+
`/api/v2/reference/fiat-institution/by-country/${countryCode}`
|
|
636
|
+
);
|
|
637
|
+
return res.data ?? res;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Create a fiat payout transaction. Handles KYC and source of funds automatically.
|
|
641
|
+
* Returns a quoted transaction — call `neutron.transactions.confirm()` to execute.
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* const txn = await neutron.fiat.payout({
|
|
645
|
+
* sourceCcy: "BTC",
|
|
646
|
+
* sourceAmount: 0.001,
|
|
647
|
+
* destCcy: "VND",
|
|
648
|
+
* destMethod: "vnd-instant",
|
|
649
|
+
* bankAcctNum: "0123456789",
|
|
650
|
+
* institutionCode: "970422",
|
|
651
|
+
* recipientName: "LE VAN A",
|
|
652
|
+
* countryCode: "VN",
|
|
653
|
+
* });
|
|
654
|
+
* // Review rate: txn.fxRate
|
|
655
|
+
* await neutron.transactions.confirm(txn.txnId);
|
|
656
|
+
*/
|
|
657
|
+
async payout(params) {
|
|
658
|
+
return this.client.post(`/api/v2/transaction`, {
|
|
659
|
+
extRefId: params.extRefId,
|
|
660
|
+
sourceReq: {
|
|
661
|
+
ccy: params.sourceCcy,
|
|
662
|
+
method: "neutronpay",
|
|
663
|
+
amtRequested: params.sourceAmount,
|
|
664
|
+
reqDetails: {}
|
|
665
|
+
},
|
|
666
|
+
destReq: {
|
|
667
|
+
ccy: params.destCcy,
|
|
668
|
+
method: params.destMethod,
|
|
669
|
+
reqDetails: {
|
|
670
|
+
bankAcctNum: params.bankAcctNum,
|
|
671
|
+
institutionCode: params.institutionCode
|
|
672
|
+
},
|
|
673
|
+
kyc: {
|
|
674
|
+
type: params.kycType || "individual",
|
|
675
|
+
details: {
|
|
676
|
+
legalFullName: params.recipientName,
|
|
677
|
+
countryCode: params.countryCode
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
sourceOfFunds: params.sourceOfFunds ?? {
|
|
682
|
+
purpose: 1,
|
|
683
|
+
source: 5,
|
|
684
|
+
relationship: 3
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/utils.ts
|
|
691
|
+
var SATS_PER_BTC = 1e8;
|
|
692
|
+
function satsToBtc(sats) {
|
|
693
|
+
return sats / SATS_PER_BTC;
|
|
694
|
+
}
|
|
695
|
+
function btcToSats(btc) {
|
|
696
|
+
return Math.round(btc * SATS_PER_BTC);
|
|
697
|
+
}
|
|
698
|
+
function formatSats(sats) {
|
|
699
|
+
return `${sats.toLocaleString("en-US")} sats`;
|
|
700
|
+
}
|
|
701
|
+
function formatBtc(btc) {
|
|
702
|
+
return `${btc.toFixed(8)} BTC`;
|
|
703
|
+
}
|
|
704
|
+
var Currency = {
|
|
705
|
+
BTC: "BTC",
|
|
706
|
+
USDT: "USDT",
|
|
707
|
+
VND: "VND",
|
|
708
|
+
USD: "USD",
|
|
709
|
+
CAD: "CAD",
|
|
710
|
+
NGN: "NGN",
|
|
711
|
+
KES: "KES",
|
|
712
|
+
GHS: "GHS"
|
|
713
|
+
};
|
|
714
|
+
var PaymentMethod = {
|
|
715
|
+
/** Internal Neutron wallet */
|
|
716
|
+
NEUTRON: "neutronpay",
|
|
717
|
+
/** Lightning Network (BOLT11 invoices) */
|
|
718
|
+
LIGHTNING: "lightning",
|
|
719
|
+
/** Lightning Address / LNURL */
|
|
720
|
+
LNURL: "lnurl",
|
|
721
|
+
/** Bitcoin on-chain */
|
|
722
|
+
ON_CHAIN: "on-chain",
|
|
723
|
+
/** USDT on TRON (TRC-20) */
|
|
724
|
+
TRON: "tron",
|
|
725
|
+
/** USDT on Ethereum (ERC-20) */
|
|
726
|
+
ETH: "eth",
|
|
727
|
+
/** Vietnamese Dong instant bank transfer */
|
|
728
|
+
VND_INSTANT: "vnd-instant"
|
|
729
|
+
};
|
|
730
|
+
var FinalStates = [
|
|
731
|
+
"completed",
|
|
732
|
+
"expired",
|
|
733
|
+
"rejected",
|
|
734
|
+
"error",
|
|
735
|
+
"usercanceled"
|
|
736
|
+
];
|
|
737
|
+
var TransactionStates = {
|
|
738
|
+
QUOTED: "quoted",
|
|
739
|
+
USER_CONFIRMED: "userconfirmed",
|
|
740
|
+
SRC_CREATED: "srccreated",
|
|
741
|
+
SRC_SENT: "srcsent",
|
|
742
|
+
SRC_INTENT: "srcintent",
|
|
743
|
+
SRC_PEND_CONFIRM: "srcpendconfirmfill",
|
|
744
|
+
SRC_CONFIRMED: "srcconfirmfilled",
|
|
745
|
+
DEST_PEND_SENT: "destpendsent",
|
|
746
|
+
DEST_SENT: "destsent",
|
|
747
|
+
COMPLETED: "completed",
|
|
748
|
+
EXPIRED: "expired",
|
|
749
|
+
REJECTED: "rejected",
|
|
750
|
+
ERROR: "error",
|
|
751
|
+
USER_CANCELED: "usercanceled"
|
|
752
|
+
};
|
|
753
|
+
var Chain = {
|
|
754
|
+
TRON: "TRON",
|
|
755
|
+
ETH: "ETH"
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// src/index.ts
|
|
759
|
+
var Neutron = class {
|
|
760
|
+
/** Account info, wallets, and deposit addresses */
|
|
761
|
+
account;
|
|
762
|
+
/** Create, confirm, list, and track transactions */
|
|
763
|
+
transactions;
|
|
764
|
+
/** Lightning invoices, payments, and utilities */
|
|
765
|
+
lightning;
|
|
766
|
+
/** Webhook management */
|
|
767
|
+
webhooks;
|
|
768
|
+
/** BTC exchange rates */
|
|
769
|
+
rates;
|
|
770
|
+
/** Fiat payouts and bank lookups */
|
|
771
|
+
fiat;
|
|
772
|
+
client;
|
|
773
|
+
constructor(config) {
|
|
774
|
+
this.client = new HttpClient(config);
|
|
775
|
+
this.account = new AccountResource(this.client);
|
|
776
|
+
this.transactions = new TransactionsResource(this.client);
|
|
777
|
+
this.lightning = new LightningResource(this.client);
|
|
778
|
+
this.webhooks = new WebhooksResource(this.client);
|
|
779
|
+
this.rates = new RatesResource(this.client);
|
|
780
|
+
this.fiat = new FiatResource(this.client);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Explicitly authenticate and verify credentials.
|
|
784
|
+
* Usually not needed — the SDK auto-authenticates on first request.
|
|
785
|
+
*/
|
|
786
|
+
async authenticate() {
|
|
787
|
+
return this.client.authenticate();
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Verify a webhook signature. Static method — no client instance needed.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* const event = Neutron.verifyWebhook(req.body, req.headers["x-neutronpay-signature"], secret);
|
|
794
|
+
*/
|
|
795
|
+
static verifyWebhook(body, signature, secret) {
|
|
796
|
+
return WebhooksResource.verifySignature(body, signature, secret);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
export {
|
|
800
|
+
AccountResource,
|
|
801
|
+
Chain,
|
|
802
|
+
Currency,
|
|
803
|
+
FiatResource,
|
|
804
|
+
FinalStates,
|
|
805
|
+
HttpClient,
|
|
806
|
+
LightningResource,
|
|
807
|
+
Neutron,
|
|
808
|
+
NeutronApiError,
|
|
809
|
+
NeutronAuthError,
|
|
810
|
+
NeutronError,
|
|
811
|
+
NeutronTimeoutError,
|
|
812
|
+
NeutronValidationError,
|
|
813
|
+
PaymentMethod,
|
|
814
|
+
RatesResource,
|
|
815
|
+
TransactionStates,
|
|
816
|
+
TransactionsResource,
|
|
817
|
+
WebhooksResource,
|
|
818
|
+
btcToSats,
|
|
819
|
+
formatBtc,
|
|
820
|
+
formatSats,
|
|
821
|
+
sanitizePathParam,
|
|
822
|
+
satsToBtc
|
|
823
|
+
};
|
|
824
|
+
//# sourceMappingURL=index.js.map
|