UNPKG

balanceofsatoshis

Version:
236 lines (199 loc) 7.29 kB
const asyncAuto = require('async/auto'); const {encode} = require('cbor'); const {genericSwapAuth} = require('goldengate'); const {genericSwapService} = require('goldengate'); const {getNetwork} = require('ln-sync'); const {getSwapMacaroon} = require('goldengate'); const {getSwapOutTerms} = require('goldengate'); const {lightningLabsSwapAuth} = require('goldengate'); const {lightningLabsSwapService} = require('goldengate'); const {paidMacaroon} = require('goldengate'); const {parsePaymentRequest} = require('ln-service'); const {payViaPaymentRequest} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {swapUserId} = require('goldengate'); const decodeSwapApiKey = require('./decode_swap_api_key'); const bitcoinNetwork = 'btc'; const bitcoinTestnetNetwork = 'btctestnet'; const bufferFromBase64 = base64 => Buffer.from(base64, 'base64'); const bufferFromHex = hex => Buffer.from(hex, 'hex'); const connectError = 'FailedToConnectToService'; const encodeCbor = json => encode(json).toString('hex'); const httpMatch = /^https?:\/\//; const {isArray} = Array; const mainnetSocket = 'https://balanceofsatoshis.com:11011'; const maxServiceFee = 1337; const maxRoutingFee = 100; const testnetSocket = 'https://balanceofsatoshis.com:11010'; /** Get a paid swap service object { fetch: <Fetch Function> lnd: <Authenticated LND gRPC API Object> [socket]: <Custom Backing Service Socket String> [token]: <Prepaid Service Token CBOR Encoded String> } @returns via cbk or Promise { id: <Authenticated User Id string> metadata: <Authenticated Service Metadata Object> paid: <Paid Tokens Number> service: <Authenticated Paid Swap Service Object> token: <Authentication Token Hex String> } */ module.exports = ({fetch, lnd, socket, token}, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!fetch) { return cbk([400, 'ExpectedFetchFunctionToGetPaidService']); } if (!lnd) { return cbk([400, 'ExpectedLndToGetPaidService']); } return cbk(); }, // Decode service token when present decodeToken: ['validate', ({}, cbk) => { // Exit early when there is no token to decode if (!token) { return cbk(); } return decodeSwapApiKey({key: token}, cbk); }], // Get network getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)], // Determine which endpoint to use for the service endpoint: ['getNetwork', ({getNetwork}, cbk) => { // Exit early when the endpoint is directly specified if (!!socket) { return cbk(null, socket); } const {metadata} = lightningLabsSwapAuth({}); const {network} = getNetwork; const {service} = lightningLabsSwapService({network}); return getSwapOutTerms({metadata, service}, err => { // Exit early when the standard endpoint is working if (!isArray(err)) { return cbk(); } const [, message] = err; // Exit early when the error is not a connection issue if (message !== connectError) { return cbk(); } // Try to switch to a backup socket when the main socket fails switch (getNetwork.network) { case bitcoinNetwork: return cbk(null, mainnetSocket); case bitcoinTestnetNetwork: return cbk(null, testnetSocket); default: return cbk(); } }); return cbk(); }], // Make a service object for the remote swap service remote: ['endpoint', 'getNetwork', ({endpoint, getNetwork}, cbk) => { // Exit early when using a generic service if (!!endpoint && httpMatch.test(endpoint)) { try { return cbk(null, genericSwapService({fetch, socket: endpoint})); } catch (err) { return cbk([500, 'UnexpectedErrorInitiatingSwapService', {err}]); } } try { return cbk(null, lightningLabsSwapService({ network: getNetwork.network, socket: endpoint, })); } catch (err) { return cbk([500, 'UnexpectedErrorInitiatingSwapService', {err}]); } }], // Get an unpaid swap macaroon getUnpaidMacaroon: [ 'decodeToken', 'remote', ({decodeToken, remote}, cbk) => { // Exit early when there is already a service token macaroon if (!!decodeToken) { return cbk(null, { id: swapUserId({macaroon: decodeToken.macaroon}).id, macaroon: decodeToken.macaroon, }); } return getSwapMacaroon({service: remote.service}, cbk); }], // Pay for the macaroon payForMacaroon: [ 'decodeToken', 'getUnpaidMacaroon', ({decodeToken, getUnpaidMacaroon}, cbk) => { // Exit early when there is already a paid service token if (!!decodeToken) { return cbk(null, {secret: decodeToken.preimage}); } const {request} = getUnpaidMacaroon; // Validate the service token payment request try { const {tokens} = parsePaymentRequest({request}); if (tokens > maxServiceFee) { return cbk([503, 'UnexpectedlyHighServiceFee', {fee: tokens}]); } } catch (err) { return cbk([503, 'FailedToParseServicePaymentRequest', {err}]); } // Pay the service token payment request to purchase the macaroon return payViaPaymentRequest({ lnd, request, max_fee: maxRoutingFee, }, cbk); }], // Create authentication metadata object metadata: [ 'decodeToken', 'endpoint', 'getUnpaidMacaroon', 'payForMacaroon', ({decodeToken, endpoint, getUnpaidMacaroon, payForMacaroon}, cbk) => { if (!decodeToken && !payForMacaroon.secret) { return cbk([400, 'FailedToPurchasePaidServiceToken']); } const {macaroon} = getUnpaidMacaroon; const preimage = payForMacaroon.secret; // Exit early when using a generic service if (!!endpoint && httpMatch.test(endpoint)) { return cbk(null, genericSwapAuth({macaroon, preimage}).metadata); } return cbk(null, lightningLabsSwapAuth({macaroon, preimage}).metadata); }], // Final service method details paidService: [ 'getUnpaidMacaroon', 'metadata', 'payForMacaroon', 'remote', ({getUnpaidMacaroon, metadata, payForMacaroon, remote}, cbk) => { const {id} = getUnpaidMacaroon; const paid = payForMacaroon.tokens; const {service} = remote; const token = encodeCbor({ macaroon: bufferFromBase64(getUnpaidMacaroon.macaroon), preimage: bufferFromHex(payForMacaroon.secret), }); return cbk(null, {id, metadata, paid, service, token}); }], }, returnResult({reject, resolve, of: 'paidService'}, cbk)); }); };