aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/ln.js
diff options
context:
space:
mode:
Diffstat (limited to 'bridge/ln.js')
-rw-r--r--bridge/ln.js116
1 files changed, 0 insertions, 116 deletions
diff --git a/bridge/ln.js b/bridge/ln.js
deleted file mode 100644
index cf2eab4..0000000
--- a/bridge/ln.js
+++ /dev/null
@@ -1,116 +0,0 @@
-// Lightning backend for the swap bridge: pay a bolt11 and report the
-// preimage. Deliberately tiny — the bridge only ever needs to *send* over
-// Lightning (an ark->LN swap), because an ASP's LN *receive* is already free.
-//
-// Two adapters, both CLN, both using named parameters:
-// 'socket' — lightning-rpc unix socket (production: mount the socket)
-// 'docker' — `docker exec <container> lightning-cli` (regtest convenience)
-
-import { execFile } from 'node:child_process';
-import { createConnection } from 'node:net';
-
-const run = (cmd, args) => new Promise((resolve, reject) => {
- execFile(cmd, args, { maxBuffer: 8 << 20 }, (err, stdout, stderr) => {
- if (err) return reject(new Error((stderr || err.message).trim().slice(0, 400)));
- resolve(stdout);
- });
-});
-
-// CLN prints progress lines before the JSON body on some commands.
-const parseJson = (s) => {
- const i = s.indexOf('{');
- if (i < 0) throw new Error('no JSON in CLN response: ' + s.slice(0, 200));
- return JSON.parse(s.slice(i));
-};
-
-function dockerAdapter({ container, network }) {
- return {
- async call(method, params = {}) {
- const args = ['exec', container, 'lightning-cli', `--${network}`, method,
- ...Object.entries(params).map(([k, v]) => `${k}=${v}`)];
- return parseJson(await run('docker', args));
- },
- };
-}
-
-function socketAdapter({ path }) {
- let id = 0;
- return {
- call(method, params = {}) {
- return new Promise((resolve, reject) => {
- const sock = createConnection(path);
- let buf = '';
- sock.on('error', reject);
- sock.on('connect', () => sock.write(JSON.stringify({
- jsonrpc: '2.0', id: ++id, method, params,
- }) + '\n'));
- sock.on('data', (d) => {
- buf += d;
- let nl;
- while ((nl = buf.indexOf('\n')) >= 0) {
- const line = buf.slice(0, nl).trim();
- buf = buf.slice(nl + 1);
- if (!line) continue;
- let msg;
- try { msg = JSON.parse(line); } catch { continue; }
- sock.end();
- if (msg.error) return reject(new Error(JSON.stringify(msg.error).slice(0, 400)));
- return resolve(msg.result);
- }
- });
- });
- },
- };
-}
-
-export function lnBackend(cfg) {
- const rpc = cfg.kind === 'docker' ? dockerAdapter(cfg) : socketAdapter(cfg);
-
- return {
- // Raw RPC passthrough — the names registrar drives offers/waitanyinvoice.
- call: (method, params) => rpc.call(method, params),
-
- async info() {
- const r = await rpc.call('getinfo');
- return { id: r.id, alias: r.alias, blockheight: r.blockheight, network: r.network };
- },
-
- // Spendable outbound capacity, so the bridge can refuse a swap it can't
- // route rather than taking an HTLC it will only have to refund.
- async outboundSat() {
- const r = await rpc.call('listpeerchannels');
- return (r.channels || [])
- .filter((c) => c.state === 'CHANNELD_NORMAL' && c.peer_connected)
- .reduce((n, c) => n + Math.floor((c.spendable_msat ?? 0) / 1000), 0);
- },
-
- // Pay a bolt11. Resolves { preimage, feeSat }; throws otherwise.
- // maxfee caps routing spend so a swap can't cost more than we charged.
- async pay(bolt11, { maxfeeSat, retryFor = 60 } = {}) {
- const params = { bolt11, retry_for: retryFor };
- if (maxfeeSat != null) params.maxfee = `${Math.max(0, Math.floor(maxfeeSat))}sat`;
- const r = await rpc.call('pay', params);
- if (r.status && r.status !== 'complete') throw new Error(`payment not complete: ${r.status}`);
- if (!r.payment_preimage) throw new Error('payment returned no preimage');
- return {
- preimage: r.payment_preimage,
- feeSat: Math.ceil((((r.amount_sent_msat ?? 0) - (r.amount_msat ?? 0)) || 0) / 1000),
- };
- },
-
- // Has this payment hash already been paid by us? Checked before paying so
- // a crashed-and-resumed bridge never double-pays.
- async paidPreimage(paymentHash) {
- const r = await rpc.call('listpays', { payment_hash: paymentHash }).catch(() => null);
- const done = (r?.pays || []).find((p) => p.status === 'complete');
- return done?.preimage || null;
- },
-
- // Is a payment still in flight? A pending pay must not be retried and
- // must not be refunded — its preimage may still arrive.
- async isPending(paymentHash) {
- const r = await rpc.call('listpays', { payment_hash: paymentHash }).catch(() => null);
- return (r?.pays || []).some((p) => p.status === 'pending');
- },
- };
-}