From e6f1139f237b85fa79629f08f93b940ebda27e3a Mon Sep 17 00:00:00 2001 From: Charles Hill Date: Wed, 24 Jul 2024 10:01:41 +0100 Subject: [PATCH] Add Blink backend --- lib/LightningBackend.js | 2 +- lib/backends/blink.js | 144 ++++++++++++++++++++++++++++++++++++++++ lib/checkBackend.js | 2 +- test/e2e/example.env | 6 ++ 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100755 lib/backends/blink.js diff --git a/lib/LightningBackend.js b/lib/LightningBackend.js index f3fee0e..22b8f54 100644 --- a/lib/LightningBackend.js +++ b/lib/LightningBackend.js @@ -12,7 +12,7 @@ class LightningBackend { const requiredOptions = this.requiredOptions.concat(classOptions.requiredOptions); this.checkRequiredOptions(options, requiredOptions); this.checkOptions && this.checkOptions(options); - this.options = options; + this.options = JSON.parse(JSON.stringify(options || {})); this.prepareCheckMethodErrorRegEx(); } diff --git a/lib/backends/blink.js b/lib/backends/blink.js new file mode 100755 index 0000000..f2e247b --- /dev/null +++ b/lib/backends/blink.js @@ -0,0 +1,144 @@ +const assert = require('assert'); +const HttpLightningBackend = require('../HttpLightningBackend'); + +// https://dev.blink.sv/ + +class Backend extends HttpLightningBackend { + + static name = 'blink'; + + constructor(options) { + options = options || {}; + super(Backend.name, options, { + defaultOptions: { + baseUrl: null, + hostname: null, + protocol: 'https', + requestContentType: 'json', + }, + requiredOptions: ['connectionString'], + }); + Object.assign(this.options, this.parseConnectionString(this.options.connectionString)); + this.options.headers['X-API-KEY'] = encodeURIComponent(this.options.apiKey); + } + + checkOptions(options) { + assert.strictEqual(typeof options.connectionString, 'string', 'Invalid option ("connectionString"): String expected'); + Object.assign(options, this.parseConnectionString(options.connectionString)); + HttpLightningBackend.prototype.checkOptions.call(this, options); + } + + parseConnectionString(connectionString) { + let values = {}; + connectionString.split(';').forEach(line => { + const [ key, value ] = line.split('='); + values[key] = value; + }); + const baseUrl = values['server'] || null; + const apiKey = values['api-key'] || null; + const walletId = values['wallet-id'] || null; + try { + assert.ok(values['type'], 'Missing "type"'); + assert.strictEqual(values['type'], 'blink', 'Invalid type: Expected "blink"'); + assert.ok(baseUrl, 'Missing "server"'); + assert.ok(apiKey, 'Missing "api-key"'); + assert.ok(walletId, 'Missing "wallet-id"'); + } catch (error) { + throw new Error(`Invalid option ("connectionString"): ${error.message}`); + } + return { baseUrl, apiKey, walletId }; + } + + payInvoice(invoice) { + const query = 'mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {\n lnInvoicePaymentSend(input: $input) {\n status\n errors {\n message\n path\n code\n }\n }\n}'; + const variables = { + input: { + paymentRequest: invoice, + walletId: this.options.walletId, + }, + }; + return this.doGraphQLQuery(query, variables).then(result => { + const { preimage } = Object.values(result.data)[0].blah || {}; + return { id: null, preimage }; + }); + } + + addInvoice(amount, extra) { + const query = 'mutation LnInvoiceCreate($input: LnInvoiceCreateInput!) {\n lnInvoiceCreate(input: $input) {\n invoice {\n paymentRequest\n paymentHash\n paymentSecret\n satoshis\n }\n errors {\n message\n }\n }\n}'; + const variables = { + input: { + amount: Math.floor(amount / 1000).toString(),// sats + walletId: this.options.walletId, + }, + }; + return this.doGraphQLQuery(query, variables).then(result => { + const { paymentRequest } = Object.values(result.data)[0].invoice || {}; + return { id: null, invoice: paymentRequest }; + }); + } + + getInvoiceStatus(paymentHash) { + return Promise.reject(new Error('Not supported by this LN service.')); + } + + getBalance() { + const query = 'query me { me { defaultAccount { wallets { id walletCurrency balance }}}}'; + const variables = {}; + return this.doGraphQLQuery(query, variables).then(result => { + const wallets = result.data.me && result.data.me.defaultAccount && result.data.me.defaultAccount.wallets || []; + assert.ok(wallets, 'Unexpected JSON response'); + const wallet = wallets.find(_wallet => _wallet.id === this.options.walletId); + assert.ok(wallet, 'Wallet info not found'); + return parseInt(wallet.balance) * 1000;// msat + }); + } + + getNodeUri() { + return Promise.reject(new Error('Not supported by this LN service.')); + } + + openChannel(remoteId, localAmt, pushAmt, makePrivate) { + return Promise.reject(new Error('Not supported by this LN service.')); + } + + doGraphQLQuery(query, variables) { + console.log('doGraphQLQuery', {query, variables}); + return this.request('post', '', { query, variables }).then(result => { + console.log(JSON.stringify({result}, null, 2)); + assert.ok(result && result.data, 'Unexpected JSON response: ' + JSON.stringify(result)); + const { errors } = Object.values(result.data)[0] || {}; + assert.ok(!errors || errors.length === 0, JSON.stringify(errors)); + return result; + }); + } +}; + +Backend.prototype.checkMethodErrorMessages = { + payInvoice: { + ok: [ + 'Unable to find a route for payment.', + 'ROUTE_FINDING_ERROR', + ], + notOk: [ + 'INSUFFICIENT_BALANCE', + 'Authorization Required', + ], + }, +}; + +Backend.form = { + label: 'Blink', + inputs: [ + { + name: 'connectionString', + label: 'BTCPay Connection String', + help: 'Sign-in to your Blink account in a browser. Go to API Keys. Click the plus symbol (+) in the upper right corner to create a new API key. Copy the BTCPay connection string for your wallet.', + type: 'password', + placeholder: 'xxx', + default: '', + required: true, + }, + ], +}; + +module.exports = Backend; diff --git a/lib/checkBackend.js b/lib/checkBackend.js index 7ec65f1..37a6347 100644 --- a/lib/checkBackend.js +++ b/lib/checkBackend.js @@ -33,7 +33,7 @@ module.exports = function(backend, config, options) { const millisatoshis = 1000; const { network } = options; let paymentSecret; - if (backend === 'lnbits') { + if (['blink', 'lnbits'].includes(backend)) { // !! IMPORTANT !! // "payment_secret" must be 32 bytes. paymentSecret = crypto.randomBytes(32); diff --git a/test/e2e/example.env b/test/e2e/example.env index 1ad3951..3357e60 100644 --- a/test/e2e/example.env +++ b/test/e2e/example.env @@ -1,3 +1,9 @@ +#TEST_BLINK_CONFIG={"connectionString":"type=blink;server=https://api.blink.sv/graphql;api-key=blink_XXX;wallet-id=xxx-yyyy-zzzz-0000-xyz123"} +#TEST_BLINK_BAD_CONFIG={"connectionString":"type=blink;server=https://api.blink.sv/graphql;api-key=blink_XXX;wallet-id=xxx-yyyy-zzzz-0000-xyz123"} +#TEST_BLINK_PAYINVOICE={"invoice":"xxx"} +#TEST_BLINK_ADDINVOICE={"amount":50000} +#TEST_BLINK_GETBALANCE={} + #TEST_CLIGHTNING_CONFIG={"unixSockPath":"xxx"} #TEST_CLIGHTNING_GETNODEURI={"result":"xxx@127.0.0.1:9735"} #TEST_CLIGHTNING_OPENCHANNEL={"remoteId":"xxx","localAmt":20000,"pushAmt":0,"makePrivate":0}