Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/HttpLightningBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class HttpLightningBackend extends LightningBackend {
assert.ok(options.baseUrl || (options.hostname && options.protocol), 'Missing required option(s): Must provide either "baseUrl" or "hostname" and "protocol"');
}

request(method, uri, data) {
request(method, uri, data, extraHeaders = {}) {
return Promise.resolve().then(() => {
const { name } = this;
data = data || {};
Expand All @@ -37,12 +37,14 @@ class HttpLightningBackend extends LightningBackend {
}
const requestUrl = `${baseUrl}${uri}`;
const parsedUrl = url.parse(requestUrl);
const finalHeaders = Object.assign(Object.assign({}, headers), extraHeaders);
let options = {
method: method.toUpperCase(),
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.path,
headers: Object.assign({}, headers),
// extraHeaders are specific to a single request, and therefore should not be set on the class-scope "options" object.
headers: finalHeaders,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ that was my thinking at least, but i could be misunderstanding how these classes work

};
if (cert) {
options.ca = cert;
Expand Down
144 changes: 144 additions & 0 deletions lib/backends/bitfinex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const assert = require('assert');
const crypto = require('crypto');
const HttpLightningBackend = require('../HttpLightningBackend');

class Backend extends HttpLightningBackend {

static name = 'bitfinex';

constructor(options) {
options = options || {};
super(Backend.name, options, {
defaultOptions: {
apiKey: null,
apiSecret: null,
baseUrl: null,
callbackUrl: null,
// Production => api.bitfinex.com
hostname: 'api.bitfinex.com',
protocol: 'https',
requestContentType: 'json',
},
requiredOptions: ['apiKey', 'apiSecret'],
});
// Headers bfx-nonce and bfx-signature are also required, but must be added individually on each request.
this.options.headers['bfx-apikey'] = this.options.apiKey;

// Bitfinex requires that this must be called once on the account before invoices can be generated.
const path = 'v2/auth/w/deposit/address';
let postData = {
wallet: 'exchange',
method: 'LNX',
};
const extraHeaders = this.generateAuthHeaders(path, postData, this.options.apiSecret)
this.request('post', `/${path}`, postData, extraHeaders).then(result => {
assert.ok(result && result.length === 8, `Unexpected response from LN Backend [POST ${path}]: data length should be 8 but is ${result.length}`);
const status = result[6];
assert.ok(status === 'SUCCESS')
});
}

checkOptions(options) {
assert.strictEqual(typeof options.apiKey, 'string', 'Invalid option ("apiKey"): String expected');
assert.strictEqual(typeof options.apiSecret, 'string', 'Invalid option ("apiSecret"): String expected');
HttpLightningBackend.prototype.checkOptions.call(this, options);
}

// https://docs.bitfinex.com/reference/rest-auth-deposit-invoice
payInvoice(invoice) {
return Promise.reject(new Error('Not supported by this LN service.'));
}

// https://docs.bitfinex.com/reference/rest-auth-deposit-invoice
addInvoice(amount, extra) {
const path = 'v2/auth/w/deposit/invoice';
let postData = {
wallet: 'exchange',
currency: 'LNX',
amount: (amount * 1.0 / 100000000000).toFixed(8) // Convert msats to BTC.
};
const extraHeaders = this.generateAuthHeaders(path, postData, this.options.apiSecret)
return this.request('post', `/${path}`, postData, extraHeaders).then(result => {
assert.ok(result && result.length === 5, `Unexpected response from LN Backend [POST ${path}]: data length should be 5 but is ${result.length}`);
const [invoiceHash, invoice, placeholder1, placeholder2, amount] = result;
return {
id: invoiceHash,
invoice: invoice,
};
});
}

getInvoiceStatus(id) {
return Promise.reject(new Error('Not supported by this LN service.'));
}

getBalance() {
return Promise.reject(new Error('Not supported by this LN service.'));
}

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.'));
}

validateResponseBody(body) {
assert.ok(body.success !== false, body.message);
}
};

Backend.prototype.generateAuthHeaders = function (path, body, apiSecret) {
const nonce = (Date.now() * 1000).toString();
let payload = `/api/${path}${nonce}${JSON.stringify(body)}`;
const signature = crypto.createHmac('sha384', apiSecret).update(payload).digest('hex');
return {
'bfx-nonce': nonce,
'bfx-signature': signature,
// bfx-apikey is already set in the constructor.
};
}

Backend.prototype.checkMethodErrorMessages = {
payInvoice: {
ok: [
'Invalid payment request',
],
notOk: [
'Socks5 proxy rejected connection - Failure',
'Invalid API key for request',
],
},
};

Backend.form = {
label: '',
inputs: [
{
name: 'hostname',
label: 'Hostname',
type: 'text',
default: 'api.bitfinex.com',
required: true,
},
{
name: 'apiKey',
label: 'API Key',
type: 'text',
placeholder: 'xxx',
default: '',
required: true,
},
{
name: 'apiSecret',
label: 'API Secret',
type: 'text',
placeholder: 'xxx',
default: '',
required: true,
},
],
};

module.exports = Backend;