From 85bac5ccdb62632ea4940511fdb69ffde00931c4 Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Fri, 28 Feb 2020 22:05:13 -0600 Subject: [PATCH] Move LND node communication to background script (#244) --- src/app/components/ChannelInfo/index.tsx | 2 +- src/app/components/ChannelList/ChannelRow.tsx | 2 +- src/app/components/SelectNode/ConfirmNode.tsx | 2 +- src/app/lib/lnd-http/utils.ts | 32 -------- src/app/modules/account/actions.ts | 2 +- src/app/modules/account/reducers.ts | 2 +- src/app/modules/account/types.ts | 2 +- src/app/modules/channels/types.ts | 2 +- src/app/modules/node/actions.ts | 8 +- src/app/modules/node/reducers.ts | 4 +- src/app/modules/node/sagas.ts | 10 +-- src/app/modules/onchain/reducers.ts | 2 +- src/app/modules/onchain/sagas.ts | 2 +- src/app/modules/payment/actions.ts | 2 +- src/app/modules/payment/reducers.ts | 2 +- src/app/modules/payment/sagas.ts | 2 +- src/app/modules/payment/types.ts | 2 +- src/app/modules/peers/types.ts | 2 +- src/app/modules/sign/sagas.ts | 2 +- src/app/utils/balances.ts | 2 +- src/app/utils/constants.ts | 4 +- src/app/utils/misc.ts | 8 +- src/app/utils/typeguards.ts | 2 +- src/background_script/getNodeInfo.ts | 2 +- src/background_script/handleLndHttp.ts | 44 +++++++++++ src/background_script/index.ts | 2 + src/{app/lib/lnd-http => lnd}/errors.ts | 0 src/{app/lib/lnd-http => lnd/http}/index.ts | 78 ++++++++++--------- src/lnd/message/index.ts | 69 ++++++++++++++++ src/{app/lib/lnd-http => lnd}/types.ts | 71 ++++++++++++++++- src/lnd/utils.ts | 50 ++++++++++++ tsconfig.json | 3 +- webpack.config.js | 1 + 33 files changed, 314 insertions(+), 106 deletions(-) delete mode 100644 src/app/lib/lnd-http/utils.ts create mode 100644 src/background_script/handleLndHttp.ts rename src/{app/lib/lnd-http => lnd}/errors.ts (100%) rename src/{app/lib/lnd-http => lnd/http}/index.ts (87%) create mode 100644 src/lnd/message/index.ts rename src/{app/lib/lnd-http => lnd}/types.ts (77%) create mode 100644 src/lnd/utils.ts diff --git a/src/app/components/ChannelInfo/index.tsx b/src/app/components/ChannelInfo/index.tsx index d578543f..548b26f6 100644 --- a/src/app/components/ChannelInfo/index.tsx +++ b/src/app/components/ChannelInfo/index.tsx @@ -7,7 +7,7 @@ import Unit from 'components/Unit'; import DetailsTable, { DetailsRow } from 'components/DetailsTable'; import TransferIcons from 'components/TransferIcons'; import Copy from 'components/Copy'; -import { CHANNEL_STATUS } from 'lib/lnd-http'; +import { CHANNEL_STATUS } from 'lnd/message'; import { AppState } from 'store/reducers'; import { getAccountInfo } from 'modules/account/actions'; import { closeChannel } from 'modules/channels/actions'; diff --git a/src/app/components/ChannelList/ChannelRow.tsx b/src/app/components/ChannelList/ChannelRow.tsx index 833f54d7..e828fdad 100644 --- a/src/app/components/ChannelList/ChannelRow.tsx +++ b/src/app/components/ChannelList/ChannelRow.tsx @@ -7,7 +7,7 @@ import Unit from 'components/Unit'; import { enumToClassName } from 'utils/formatters'; import { channelStatusText } from 'utils/constants'; import { ChannelWithNode } from 'modules/channels/types'; -import { CHANNEL_STATUS } from 'lib/lnd-http'; +import { CHANNEL_STATUS } from 'lnd/message'; import './ChannelRow.less'; interface Props { diff --git a/src/app/components/SelectNode/ConfirmNode.tsx b/src/app/components/SelectNode/ConfirmNode.tsx index 19df6a93..9419f60d 100644 --- a/src/app/components/SelectNode/ConfirmNode.tsx +++ b/src/app/components/SelectNode/ConfirmNode.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from 'antd'; -import { GetInfoResponse } from 'lib/lnd-http'; +import { GetInfoResponse } from 'lnd/message'; import { blockchainDisplayName, CHAIN_TYPE } from 'utils/constants'; import './ConfirmNode.less'; diff --git a/src/app/lib/lnd-http/utils.ts b/src/app/lib/lnd-http/utils.ts deleted file mode 100644 index dfbc66f7..00000000 --- a/src/app/lib/lnd-http/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as Errors from './errors'; -import { ErrorResponse } from './types'; - -export function parseNodeErrorResponse(res: ErrorResponse): Error { - if (res.error.includes('expected 1 macaroon')) { - return new Errors.MacaroonAuthError('Macaroon is required'); - } - - if (res.error.includes('permission denied')) { - return new Errors.PermissionDeniedError('You lack permission to do that'); - } - - if (res.error.includes('unable to find a path to destination')) { - return new Errors.NoRouteError('No route available for payment'); - } - - if (res.error.includes('already connected to peer')) { - return new Errors.AlreadyConnectedError('You are already peers with that node'); - } - - return new Errors.UnknownServerError(res.error); -} - -export function txIdBytesToHex(txbytes: string) { - const txbinary = Buffer.from(txbytes, 'base64').toString('binary'); - const txarray = new Uint8Array(txbinary.length); - for (let i = 0; i < txbinary.length; i++) { - txarray[i] = txbinary.charCodeAt(i); - } - txarray.reverse(); - return new Buffer(txarray).toString('hex'); -} diff --git a/src/app/modules/account/actions.ts b/src/app/modules/account/actions.ts index 2375c3b4..9e0fcbe5 100644 --- a/src/app/modules/account/actions.ts +++ b/src/app/modules/account/actions.ts @@ -1,4 +1,4 @@ -import { NewAddressArguments } from 'lib/lnd-http'; +import { NewAddressArguments } from 'lnd/message'; import types from './types'; export function getAccountInfo() { diff --git a/src/app/modules/account/reducers.ts b/src/app/modules/account/reducers.ts index f83bc8ae..5388775e 100644 --- a/src/app/modules/account/reducers.ts +++ b/src/app/modules/account/reducers.ts @@ -1,5 +1,5 @@ import types, { Account, LightningPaymentWithToNode } from './types'; -import { LightningInvoice, ChainTransaction } from 'lib/lnd-http'; +import { LightningInvoice, ChainTransaction } from 'lnd/message'; export interface AccountState { account: Account | null; diff --git a/src/app/modules/account/types.ts b/src/app/modules/account/types.ts index 222d7a91..9125a401 100644 --- a/src/app/modules/account/types.ts +++ b/src/app/modules/account/types.ts @@ -3,7 +3,7 @@ import { LightningInvoice, LightningPayment, ChainTransaction, -} from 'lib/lnd-http'; +} from 'lnd/message'; enum AccountTypes { GET_ACCOUNT_INFO = 'GET_ACCOUNT_INFO', diff --git a/src/app/modules/channels/types.ts b/src/app/modules/channels/types.ts index e8b0131d..9c3f874f 100644 --- a/src/app/modules/channels/types.ts +++ b/src/app/modules/channels/types.ts @@ -5,7 +5,7 @@ import { ForceClosingChannel, WaitingChannel, LightningNode, -} from 'lib/lnd-http'; +} from 'lnd/message'; enum ChannelsTypes { GET_CHANNELS = 'GET_CHANNELS', diff --git a/src/app/modules/node/actions.ts b/src/app/modules/node/actions.ts index 585c4b97..23699d43 100644 --- a/src/app/modules/node/actions.ts +++ b/src/app/modules/node/actions.ts @@ -1,4 +1,4 @@ -import LndHttpClient, { Macaroon } from 'lib/lnd-http'; +import LndMessageClient, { Macaroon } from 'lnd/message'; import { selectSyncedUnencryptedNodeState, selectSyncedEncryptedNodeState, @@ -63,7 +63,7 @@ export function setNode( url, adminMacaroon, readonlyMacaroon, - lib: new LndHttpClient(url, adminMacaroon), + lib: new LndMessageClient(url, adminMacaroon), }, }; } @@ -81,7 +81,7 @@ export function setSyncedUnencryptedNodeState( payload: { url, readonlyMacaroon, - lib: url ? new LndHttpClient(url as string, readonlyMacaroon as string) : null, + lib: url ? new LndMessageClient(url as string, readonlyMacaroon as string) : null, }, }; } @@ -95,7 +95,7 @@ export function setSyncedEncryptedNodeState( payload: { url, adminMacaroon, - lib: url ? new LndHttpClient(url as string, adminMacaroon as string) : null, + lib: url ? new LndMessageClient(url as string, adminMacaroon as string) : null, }, }; } diff --git a/src/app/modules/node/reducers.ts b/src/app/modules/node/reducers.ts index 7cce6aad..f5601d4b 100644 --- a/src/app/modules/node/reducers.ts +++ b/src/app/modules/node/reducers.ts @@ -1,9 +1,9 @@ -import LndHttpClient, { Macaroon, GetInfoResponse } from 'lib/lnd-http'; +import { Macaroon, GetInfoResponse, LndAPI } from 'lnd/message'; import types from './types'; import settingsTypes from 'modules/settings/types'; export interface NodeState { - lib: LndHttpClient | null; + lib: LndAPI | null; url: string | null; isNodeChecked: boolean; adminMacaroon: Macaroon | null; diff --git a/src/app/modules/node/sagas.ts b/src/app/modules/node/sagas.ts index bcd2a93a..13477159 100644 --- a/src/app/modules/node/sagas.ts +++ b/src/app/modules/node/sagas.ts @@ -11,12 +11,12 @@ import { import { requirePassword } from 'modules/crypto/sagas'; import { accountTypes } from 'modules/account'; import { channelsTypes } from 'modules/channels'; -import LndHttpClient, { MacaroonAuthError, PermissionDeniedError } from 'lib/lnd-http'; +import LndMessageClient, { MacaroonAuthError, PermissionDeniedError } from 'lnd/message'; import types from './types'; export function* handleCheckNode(action: ReturnType) { const url = action.payload; - const client = new LndHttpClient(url); + const client = new LndMessageClient(url); try { yield call(client.getInfo); } catch (err) { @@ -36,7 +36,7 @@ export function* handleCheckNodes(action: ReturnType) const requests = urls.map(url => { return new Promise(async resolve => { try { - const client = new LndHttpClient(url); + const client = new LndMessageClient(url); await client.getInfo(); resolve(url); } catch (err) { @@ -65,7 +65,7 @@ export function* handleCheckAuth(action: ReturnType) { const { url, admin, readonly } = action.payload; // Check read-only by making sure request doesn't error - let client = new LndHttpClient(url, readonly); + let client = new LndMessageClient(url, readonly); let nodeInfo; try { nodeInfo = yield call(client.getInfo); @@ -81,7 +81,7 @@ export function* handleCheckAuth(action: ReturnType) { // Test admin by intentionally send an invalid payment, // but make sure we didn't error out with a macaroon auth error // TODO: Replace with sign message once REST supports it - client = new LndHttpClient(url, admin); + client = new LndMessageClient(url, admin); try { yield call(client.sendPayment, { payment_request: 'testing admin' }); } catch (err) { diff --git a/src/app/modules/onchain/reducers.ts b/src/app/modules/onchain/reducers.ts index bb101676..326f1d64 100644 --- a/src/app/modules/onchain/reducers.ts +++ b/src/app/modules/onchain/reducers.ts @@ -1,5 +1,5 @@ import types from './types'; -import { Utxo } from 'lib/lnd-http'; +import { Utxo } from 'lnd/message'; export interface OnChainState { utxos: Utxo[] | null; diff --git a/src/app/modules/onchain/sagas.ts b/src/app/modules/onchain/sagas.ts index eea6c36e..1b34459e 100644 --- a/src/app/modules/onchain/sagas.ts +++ b/src/app/modules/onchain/sagas.ts @@ -1,7 +1,7 @@ import { SagaIterator } from 'redux-saga'; import { takeLatest, select, call, put } from 'redux-saga/effects'; import { selectNodeLibOrThrow } from 'modules/node/selectors'; -import { GetUtxosResponse } from 'lib/lnd-http/types'; +import { GetUtxosResponse } from 'lnd/types'; import types from './types'; export function* handleGetUtxos() { diff --git a/src/app/modules/payment/actions.ts b/src/app/modules/payment/actions.ts index 72f3af5c..fb8098fc 100644 --- a/src/app/modules/payment/actions.ts +++ b/src/app/modules/payment/actions.ts @@ -2,7 +2,7 @@ import { SendPaymentArguments, CreateInvoiceArguments, SendOnChainArguments, -} from 'lib/lnd-http'; +} from 'lnd/message'; import types from './types'; export function checkPaymentRequest(paymentRequest: string, amount?: string) { diff --git a/src/app/modules/payment/reducers.ts b/src/app/modules/payment/reducers.ts index 2f88bb5b..ac569f21 100644 --- a/src/app/modules/payment/reducers.ts +++ b/src/app/modules/payment/reducers.ts @@ -2,7 +2,7 @@ import { SendPaymentResponse, CreateInvoiceResponse, SendOnChainResponse, -} from 'lib/lnd-http'; +} from 'lnd/message'; import types, { PaymentRequestState, OnChainFeeEstimates } from './types'; export interface PaymentState { diff --git a/src/app/modules/payment/sagas.ts b/src/app/modules/payment/sagas.ts index 1275cf3b..f6b9c509 100644 --- a/src/app/modules/payment/sagas.ts +++ b/src/app/modules/payment/sagas.ts @@ -11,7 +11,7 @@ import { checkPaymentRequest, sendPayment, createInvoice, sendOnChain } from './ import { apiFetchOnChainFees } from 'lib/earn'; import types from './types'; import { CHAIN_TYPE } from 'utils/constants'; -import { NoRouteError } from 'lib/lnd-http/errors'; +import { NoRouteError } from 'lnd/errors'; export function* handleSendPayment(action: ReturnType) { try { diff --git a/src/app/modules/payment/types.ts b/src/app/modules/payment/types.ts index 5a927cdc..cdf5827b 100644 --- a/src/app/modules/payment/types.ts +++ b/src/app/modules/payment/types.ts @@ -1,4 +1,4 @@ -import { DecodePaymentRequestResponse, LightningNode, Route } from 'lib/lnd-http'; +import { DecodePaymentRequestResponse, LightningNode, Route } from 'lnd/message'; enum PaymentTypes { CHECK_PAYMENT_REQUEST = 'CHECK_PAYMENT_REQUEST', diff --git a/src/app/modules/peers/types.ts b/src/app/modules/peers/types.ts index b75bd001..bf7da630 100644 --- a/src/app/modules/peers/types.ts +++ b/src/app/modules/peers/types.ts @@ -1,4 +1,4 @@ -import { Peer, LightningNode } from 'lib/lnd-http'; +import { Peer, LightningNode } from 'lnd/message'; enum PeersTypes { GET_PEERS = 'GET_PEERS_INFO', diff --git a/src/app/modules/sign/sagas.ts b/src/app/modules/sign/sagas.ts index c7d85404..b5017c35 100644 --- a/src/app/modules/sign/sagas.ts +++ b/src/app/modules/sign/sagas.ts @@ -8,7 +8,7 @@ import types from './types'; import { SignMessageResponse as LndSignMessageResponse, VerifyMessageResponse as LndVerifyMessageResponse, -} from 'lib/lnd-http/types'; +} from 'lnd/types'; import { safeGetNodeInfo } from 'utils/misc'; export function* handleSignMessage(action: ReturnType) { diff --git a/src/app/utils/balances.ts b/src/app/utils/balances.ts index 045774e4..f96d3a54 100644 --- a/src/app/utils/balances.ts +++ b/src/app/utils/balances.ts @@ -2,7 +2,7 @@ import BN from 'bn.js'; import moment from 'moment'; import { ChannelWithNode } from 'modules/channels/types'; -import { Utxo, CHANNEL_STATUS } from 'lib/lnd-http'; +import { Utxo, CHANNEL_STATUS } from 'lnd/message'; export interface BalanceStats { total: string; diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index fe21cc6b..224f3ca0 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -4,8 +4,8 @@ import DecredLogo from 'static/images/decred.svg'; import GroestlcoinLogo from 'static/images/groestlcoin.svg'; import * as React from 'react'; import { CustomIconComponentProps } from 'antd/lib/icon'; -import { CHANNEL_STATUS } from 'lib/lnd-http'; -import { AddressType } from 'lib/lnd-http/types'; +import { CHANNEL_STATUS } from 'lnd/message'; +import { AddressType } from 'lnd/types'; export enum NODE_TYPE { LOCAL = 'LOCAL', diff --git a/src/app/utils/misc.ts b/src/app/utils/misc.ts index d0db44b5..6074e45a 100644 --- a/src/app/utils/misc.ts +++ b/src/app/utils/misc.ts @@ -1,9 +1,9 @@ import { - LndHttpClient, GetNodeInfoResponse, AlreadyConnectedError, LightningNode, -} from 'lib/lnd-http'; + LndAPI, +} from 'lnd/message'; export function sleep(time: number) { return new Promise(resolve => { @@ -29,7 +29,7 @@ export const UNKNOWN_NODE: LightningNode = { // Run getNodeInfo, but if it fails, return a spoofed node object export async function safeGetNodeInfo( - lib: LndHttpClient, + lib: LndAPI, pubkey: string, ): Promise { if (!pubkey) { @@ -56,7 +56,7 @@ export async function safeGetNodeInfo( } // Run connectPeer, but if it fails due to duplicate, just ignore -export async function safeConnectPeer(lib: LndHttpClient, address: string): Promise { +export async function safeConnectPeer(lib: LndAPI, address: string): Promise { try { lib.connectPeer(address); } catch (err) { diff --git a/src/app/utils/typeguards.ts b/src/app/utils/typeguards.ts index 6d6b30f0..07c98706 100644 --- a/src/app/utils/typeguards.ts +++ b/src/app/utils/typeguards.ts @@ -1,5 +1,5 @@ import { AnyTransaction, LightningPaymentWithToNode } from 'modules/account/types'; -import { LightningInvoice, ChainTransaction } from 'lib/lnd-http'; +import { LightningInvoice, ChainTransaction } from 'lnd/message'; export function isInvoice(source: AnyTransaction): source is LightningInvoice { return !!(source as LightningInvoice).expiry; diff --git a/src/background_script/getNodeInfo.ts b/src/background_script/getNodeInfo.ts index a386fd63..b1c4cc1b 100644 --- a/src/background_script/getNodeInfo.ts +++ b/src/background_script/getNodeInfo.ts @@ -1,6 +1,6 @@ import { GetInfoResponse } from 'webln'; import runSelector from '../content_script/runSelector'; -import { LndHttpClient } from 'lib/lnd-http'; +import { LndHttpClient } from 'lnd/http'; import { selectSyncedUnencryptedNodeState } from 'modules/node/selectors'; export default async function getNodeInfo(): Promise { diff --git a/src/background_script/handleLndHttp.ts b/src/background_script/handleLndHttp.ts new file mode 100644 index 00000000..1e7306b3 --- /dev/null +++ b/src/background_script/handleLndHttp.ts @@ -0,0 +1,44 @@ +import { browser } from 'webextension-polyfill-ts'; +import { + LndHttpClient, + LndAPIRequestMessage, + LndAPIResponseMessage, + LndAPIMethod, + LndAPIResponseError, +} from '../lnd/http'; + +function isLndRequestMessage(req: any): req is LndAPIRequestMessage { + if (req && req.type === 'lnd-api-request') { + return true; + } + return false; +} + +export default function handleLndHttp() { + // Background manages communication between page and its windows + browser.runtime.onMessage.addListener(async (request: unknown) => { + if (!isLndRequestMessage(request)) { + return; + } + + const client = new LndHttpClient(request.url, request.macaroon); + const fn = client[request.method] as LndHttpClient[typeof request.method]; + const args = request.args as Parameters; + + return (fn as any)(...args) + .then((data: ReturnType) => { + return { + type: 'lnd-api-response', + method: request.method, + data, + } as LndAPIResponseMessage; + }) + .catch((err: LndAPIResponseError) => { + return { + type: 'lnd-api-response', + method: request.method, + error: err, + } as LndAPIResponseMessage; + }); + }); +} diff --git a/src/background_script/index.ts b/src/background_script/index.ts index 9354fdcb..ffb5f8a5 100755 --- a/src/background_script/index.ts +++ b/src/background_script/index.ts @@ -1,8 +1,10 @@ +import handleLndHttp from './handleLndHttp'; import handlePrompts from './handlePrompts'; import handlePassword from './handlePassword'; import handleContextMenu from './handleContextMenu'; function initBackground() { + handleLndHttp(); handlePrompts(); handlePassword(); handleContextMenu(); diff --git a/src/app/lib/lnd-http/errors.ts b/src/lnd/errors.ts similarity index 100% rename from src/app/lib/lnd-http/errors.ts rename to src/lnd/errors.ts diff --git a/src/app/lib/lnd-http/index.ts b/src/lnd/http/index.ts similarity index 87% rename from src/app/lib/lnd-http/index.ts rename to src/lnd/http/index.ts index 0eee17a2..45ca7521 100644 --- a/src/app/lib/lnd-http/index.ts +++ b/src/lnd/http/index.ts @@ -1,13 +1,12 @@ import { stringify } from 'query-string'; -import { parseNodeErrorResponse, txIdBytesToHex } from './utils'; -import { NetworkError, SendTransactionError } from './errors'; -import * as T from './types'; -export * from './errors'; -export * from './types'; +import { txIdBytesToHex } from '../utils'; +import * as T from '../types'; +export * from '../errors'; +export * from '../types'; export type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; -export class LndHttpClient { +export class LndHttpClient implements T.LndAPI { url: string; macaroon: undefined | T.Macaroon; @@ -260,7 +259,8 @@ export class LndHttpClient { args, ).then(res => { if (res.payment_error) { - throw new SendTransactionError(res.payment_error); + // Make it easy to convert on the other side + throw new Error(`SendTransactionError: ${res.payment_error}`); } return { ...res, @@ -367,12 +367,12 @@ export class LndHttpClient { }; // Internal fetch function - protected request( + protected async request( method: ApiMethod, path: string, args?: A, defaultValues?: Partial, - ): T.Response { + ): Promise { let body = null; let query = ''; const headers = new Headers(); @@ -390,36 +390,42 @@ export class LndHttpClient { headers.append('Grpc-Metadata-macaroon', this.macaroon); } - return fetch(this.url + path + query, { - method, - headers, - body, - }) - .then(async res => { - if (!res.ok) { - let errBody: any; - try { - errBody = await res.json(); - if (!errBody.error) throw new Error(); - } catch (err) { - throw new NetworkError(res.statusText, res.status); + try { + const res = await fetch(this.url + path + query, { + method, + headers, + body, + }); + if (!res.ok) { + let errBody: any; + try { + errBody = await res.json(); + if (!errBody.error) { + throw new Error(); } - const error = parseNodeErrorResponse(errBody); - throw error; - } - return res.json(); - }) - .then((res: Partial) => { - if (defaultValues) { - // TS can't handle generic spreadables - return { ...(defaultValues as any), ...(res as any) } as R; + } catch (err) { + throw { + statusText: res.statusText, + status: res.status, + } as T.LndAPIResponseError; } - return res as R; - }) - .catch(err => { - console.error(`API error calling ${method} ${path}`, err); + console.log('errBody', errBody); + throw errBody as T.LndAPIResponseError; + } + const json = await res.json(); + if (defaultValues) { + // TS can't handle generic spreadables + return { ...(defaultValues as any), ...(json as any) } as R; + } + return json as R; + } catch (err) { + console.error(`API error calling ${method} ${path}`, err); + // Thrown errors must be JSON serializable, so include metadata if possible + if (err.code || err.status || !err.message) { throw err; - }); + } + throw err.message; + } } } diff --git a/src/lnd/message/index.ts b/src/lnd/message/index.ts new file mode 100644 index 00000000..6ab732c4 --- /dev/null +++ b/src/lnd/message/index.ts @@ -0,0 +1,69 @@ +import { browser } from 'webextension-polyfill-ts'; +import { parseResponseError } from '../utils'; +import * as T from '../types'; +export * from '../types'; +export * from '../errors'; + +// TS doesn't like dynamically constructed class methods +class LndMessageClient implements T.LndAPI { + url: string; + macaroon: undefined | T.Macaroon; + + constructor(url: string, macaroon?: T.Macaroon) { + // Remove trailing slash for consistency + this.url = url.replace(/\/$/, ''); + this.macaroon = macaroon; + } + + // Manually re-implement the methods, tedious but that's TS life for ya + getInfo = (...args: any[]) => this.request('getInfo', args) as any; + getNodeInfo = (...args: any[]) => this.request('getNodeInfo', args) as any; + getChannels = (...args: any[]) => this.request('getChannels', args) as any; + getPendingChannels = (...args: any[]) => + this.request('getPendingChannels', args) as any; + getBlockchainBalance = (...args: any[]) => + this.request('getBlockchainBalance', args) as any; + getChannelsBalance = (...args: any[]) => + this.request('getChannelsBalance', args) as any; + getTransactions = (...args: any[]) => this.request('getTransactions', args) as any; + getPayments = (...args: any[]) => this.request('getPayments', args) as any; + getInvoices = (...args: any[]) => this.request('getInvoices', args) as any; + getInvoice = (...args: any[]) => this.request('getInvoice', args) as any; + createInvoice = (...args: any[]) => this.request('createInvoice', args) as any; + decodePaymentRequest = (...args: any[]) => + this.request('decodePaymentRequest', args) as any; + queryRoutes = (...args: any[]) => this.request('queryRoutes', args) as any; + sendPayment = (...args: any[]) => this.request('sendPayment', args) as any; + sendOnChain = (...args: any[]) => this.request('sendOnChain', args) as any; + getAddress = (...args: any[]) => this.request('getAddress', args) as any; + getPeers = (...args: any[]) => this.request('getPeers', args) as any; + connectPeer = (...args: any[]) => this.request('connectPeer', args) as any; + openChannel = (...args: any[]) => this.request('openChannel', args) as any; + closeChannel = (...args: any[]) => this.request('closeChannel', args) as any; + signMessage = (...args: any[]) => this.request('signMessage', args) as any; + verifyMessage = (...args: any[]) => this.request('verifyMessage', args) as any; + getUtxos = (...args: any[]) => this.request('getUtxos', args) as any; + + // Internal request function, sends a message to the background context and + // returns its response. + protected async request( + method: M, + args: any, + ): Promise> { + const message: T.LndAPIRequestMessage = { + type: 'lnd-api-request', + url: this.url, + macaroon: this.macaroon, + method, + args, + }; + const res: T.LndAPIResponseMessage = await browser.runtime.sendMessage(message); + console.log(res); + if (res.data) { + return res.data; + } + throw parseResponseError(res.error || 'Unknown response from extension'); + } +} + +export default LndMessageClient; diff --git a/src/app/lib/lnd-http/types.ts b/src/lnd/types.ts similarity index 77% rename from src/app/lib/lnd-http/types.ts rename to src/lnd/types.ts index 9dd70c15..a3bd272f 100644 --- a/src/app/lib/lnd-http/types.ts +++ b/src/lnd/types.ts @@ -1,8 +1,6 @@ // Shared Types export type Macaroon = string; -export type Response = Promise; - export type AddressType = '0' | '2'; export type UtxoAddressType = 'NESTED_PUBKEY_HASH' | 'WITNESS_PUBKEY_HASH'; @@ -407,3 +405,72 @@ export interface GetUtxosParams { export interface GetUtxosResponse { utxos: Utxo[]; } + +// Shared API interface +export interface LndAPI { + getInfo(): Promise; + getNodeInfo(pubKey: string): Promise; + getChannels(): Promise; + getPendingChannels(): Promise; + getBlockchainBalance(): Promise; + getChannelsBalance(): Promise; + getTransactions(): Promise; + getPayments(): Promise; + getInvoices(args?: GetInvoicesArguments): Promise; + getInvoice(paymentHash: string): Promise; + createInvoice(args: CreateInvoiceArguments): Promise; + decodePaymentRequest(paymentRequest: string): Promise; + queryRoutes( + pubKey: string, + amount: string, + args: QueryRoutesArguments, + ): Promise; + sendPayment(args: SendPaymentArguments): Promise; + sendOnChain(args: SendOnChainArguments): Promise; + getAddress(args: NewAddressArguments): Promise; + getPeers(): Promise; + connectPeer(address: string, perm?: boolean): Promise<{}>; + openChannel(params: OpenChannelParams): Promise; + closeChannel(fundingTxid: string, outputIndex: string): Promise; + signMessage(message: string): Promise; + verifyMessage(params: VerifyMessageParams): Promise; + getUtxos(params?: GetUtxosParams): Promise; +} + +export type LndAPIMethod = keyof LndAPI; + +// Browser message interface +export interface LndAPIRequestMessage { + type: 'lnd-api-request'; + url: string; + macaroon: undefined | Macaroon; + method: M; + args: Parameters; +} + +export interface LndAPIResponseNetworkError { + statusText: string; + status: number; +} + +export type LndAPIResponseError = + // Fetch error + | LndAPIResponseNetworkError + // LND error + | ErrorResponse + // Generic error + | string; + +export type LndAPIResponseMessage = + | { + type: 'lnd-api-response'; + method: M; + data: ReturnType; + error: undefined; + } + | { + type: 'lnd-api-response'; + method: M; + error: LndAPIResponseError; + data: undefined; + }; diff --git a/src/lnd/utils.ts b/src/lnd/utils.ts new file mode 100644 index 00000000..066c4340 --- /dev/null +++ b/src/lnd/utils.ts @@ -0,0 +1,50 @@ +import * as Errors from './errors'; +import { LndAPIResponseNetworkError, LndAPIResponseError } from './types'; + +const isNetworkError = ( + error: LndAPIResponseError, +): error is LndAPIResponseNetworkError => + (error as LndAPIResponseNetworkError).statusText !== undefined; + +export function parseResponseError(err: LndAPIResponseError): Error { + console.log(err); + if (typeof err === 'string') { + return new Error(err); + } + + if (isNetworkError(err)) { + return new Errors.NetworkError(err.statusText, err.status); + } + + if (err.error.includes('SendTransactionError')) { + return new Errors.SendTransactionError(err.error.split('SendTransactionError: ')[1]); + } + + if (err.error.includes('expected 1 macaroon')) { + return new Errors.MacaroonAuthError('Macaroon is required'); + } + + if (err.error.includes('permission denied')) { + return new Errors.PermissionDeniedError('You lack permission to do that'); + } + + if (err.error.includes('unable to find a path to destination')) { + return new Errors.NoRouteError('No route available for payment'); + } + + if (err.error.includes('already connected to peer')) { + return new Errors.AlreadyConnectedError('You are already peers with that node'); + } + + return new Errors.UnknownServerError(err.error); +} + +export function txIdBytesToHex(txbytes: string) { + const txbinary = Buffer.from(txbytes, 'base64').toString('binary'); + const txarray = new Uint8Array(txbinary.length); + for (let i = 0; i < txbinary.length; i++) { + txarray[i] = txbinary.charCodeAt(i); + } + txarray.reverse(); + return new Buffer(txarray).toString('hex'); +} diff --git a/tsconfig.json b/tsconfig.json index a33b4eb9..0ebfe7ea 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "store/*": ["./src/app/store/*"], "style/*": ["./src/app/style/*"], "typings/*": ["./src/app/typings/*"], - "utils/*": ["./src/app/utils/*"] + "utils/*": ["./src/app/utils/*"], + "lnd/*": ["./src/lnd/*"] } }, "include": ["src", "webpack.config.ts"] diff --git a/webpack.config.js b/webpack.config.js index df9e82d6..ecdd2d9d 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -160,6 +160,7 @@ module.exports = { style: `${srcApp}/style`, typings: `${srcApp}/typings`, utils: `${srcApp}/utils`, + lnd: `${src}/lnd`, }, }, plugins: [