UNPKG

paid-services

Version:
156 lines (134 loc) 4.72 kB
const asyncAuto = require('async/auto'); const asyncRetry = require('async/retry'); const asyncTimeout = require('async/timeout'); const {connectPeer} = require('ln-sync'); const {getChainBalance} = require('ln-service'); const {getNodeAlias} = require('ln-sync'); const {returnResult} = require('asyncjs-util'); const {getGroupDetails} = require('./p2p'); const coordinatorFromJoinCode = n => n.slice(0, 66); const defaultConnectRetryMs = 1000 * 60; const groupIdFromJoinCode = n => n.slice(66); const interval = 500; const isCode = n => !!n && n.length === 98; const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n); const niceName = n => `${n.alias} ${n.id}`.trim(); const times = 2 * 60; const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8); /** Ask to confirm joining a group { ask: <Ask Function> lnd: <Authenticated LND API Object> } @returns via cbk or Promise { capacity: <Channel Capacity Tokens Number> coordinator: <Group Coordinator Identity Public Key Hex String> count: <Group Members Count> id: <Group Id Hex String> rate: <Chain Fee Tokens Per VByte Number> } */ module.exports = ({ask, lnd}, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!ask) { return cbk([400, 'ExpectedAskFunctionToConfirmGroupJoin']); } if (!lnd) { return cbk([400, 'ExpectedAuthenticatedLndToConfirmGroupJoin']); } return cbk(); }, // Ask for the group entry code askForCode: ['validate', ({}, cbk) => { return ask({ name: 'code', message: 'Enter a group join code to join a group', validate: input => !!isCode(input), }, ({code}) => cbk(null, code)); }], // Get the wallet balance to make sure there are enough funds to join getBalance: ['validate', ({}, cbk) => getChainBalance({lnd}, cbk)], // Parse the group join code group: ['askForCode', ({askForCode}, cbk) => { const coordinator = coordinatorFromJoinCode(askForCode); if (!isPublicKey(coordinator)) { return cbk([400, 'ExpectedValidGroupJoinCodeToRequestGroupDetails']); } const id = groupIdFromJoinCode(askForCode); return cbk(null, {id, coordinator}) }], // Connect to the coordinator connect: ['group', ({group}, cbk) => { return asyncRetry({interval, times}, cbk => { return asyncTimeout(connectPeer, defaultConnectRetryMs)({ lnd, id: group.coordinator, }, cbk); }, cbk); }], // Get the coordinator node alias getAlias: ['group', ({group}, cbk) => { return getNodeAlias({lnd, id: group.coordinator}, cbk); }], // Get the group details from the coordinator getDetails: ['connect', 'group', ({group}, cbk) => { return asyncRetry({interval, times}, cbk => { return asyncTimeout(getGroupDetails, defaultConnectRetryMs)({ lnd, coordinator: group.coordinator, id: group.id, }, cbk); }, cbk); }], // Confirm the group join ok: [ 'getAlias', 'getBalance', 'getDetails', ({getAlias, getBalance, getDetails}, cbk) => { // Check to make sure that there are on chain funds for this group if (getBalance.chain_balance < getDetails.funding) { return cbk([ 400, 'InsufficientChainFundsAvailableToJoinGroup', {channel_capacity: tokensAsBigUnit(getDetails.capacity)}, ]); } const coordinatedBy = `coordinated by ${niceName(getAlias)}`; const members = `${getDetails.count} member group`; const size = `with ${tokensAsBigUnit(getDetails.capacity)} channels`; const rate = `${getDetails.rate}/vbyte chain fee`; return ask({ name: 'join', message: `Join ${members} ${size} at ${rate}, ${coordinatedBy}?`, type: 'confirm', }, ({join}) => cbk(null, join)); }], // Go ahead with the group join join: ['getDetails', 'group', 'ok', ({getDetails, group, ok}, cbk) => { if (!ok) { return cbk([400, 'CanceledGroupChannelJoin']); } return cbk(null, { capacity: getDetails.capacity, coordinator: group.coordinator, count: getDetails.count, id: group.id, rate: getDetails.rate, }); }], }, returnResult({reject, resolve, of: 'join'}, cbk)); }); };