@@ -8,11 +8,12 @@ import { InvoicesClient } from '../proto/lndinvoices_grpc_pb';
88import * as lndrpc from '../proto/lndrpc_pb' ;
99import * as lndinvoices from '../proto/lndinvoices_pb' ;
1010import assert from 'assert' ;
11- import { promises as fs } from 'fs' ;
11+ import { promises as fs , watch } from 'fs' ;
1212import { SwapState , SwapRole , SwapClientType } from '../constants/enums' ;
1313import { SwapDeal } from '../swaps/types' ;
1414import { base64ToHex , hexToUint8Array } from '../utils/utils' ;
1515import { LndClientConfig , LndInfo , ChannelCount , Chain } from './types' ;
16+ import path from 'path' ;
1617
1718interface LightningMethodIndex extends LightningClient {
1819 [ methodName : string ] : Function ;
@@ -34,6 +35,7 @@ interface LndClient {
3435}
3536
3637const MAXFEE = 0.03 ;
38+
3739/** A class representing a client to interact with lnd. */
3840class LndClient extends SwapClient {
3941 public readonly type = SwapClientType . Lnd ;
@@ -44,7 +46,8 @@ class LndClient extends SwapClient {
4446 private lightning ?: LightningClient | LightningMethodIndex ;
4547 private walletUnlocker ?: WalletUnlockerClient | InvoicesMethodIndex ;
4648 private invoices ?: InvoicesClient | InvoicesMethodIndex ;
47- private meta ! : grpc . Metadata ;
49+ private macaroonpath ?: string ;
50+ private meta = new grpc . Metadata ( ) ;
4851 private uri ! : string ;
4952 private credentials ! : ChannelCredentials ;
5053 /** The identity pub key for this lnd instance. */
@@ -56,6 +59,8 @@ class LndClient extends SwapClient {
5659 private channelSubscription ?: ClientReadableStream < lndrpc . ChannelEventUpdate > ;
5760 private invoiceSubscriptions = new Map < string , ClientReadableStream < lndrpc . Invoice > > ( ) ;
5861 private maximumOutboundAmount = 0 ;
62+ private initWalletResolve ?: ( value : boolean ) => void ;
63+ private watchMacaroonResolve ?: ( value : boolean ) => void ;
5964
6065 private static MINUTES_PER_BLOCK_BY_CURRENCY : { [ key : string ] : number } = {
6166 BTC : 10 ,
@@ -77,12 +82,27 @@ class LndClient extends SwapClient {
7782 this . finalLock = Math . round ( 400 / LndClient . MINUTES_PER_BLOCK_BY_CURRENCY [ currency ] ) ;
7883 }
7984
85+ private static waitForClientReady = ( client : grpc . Client ) => {
86+ return new Promise ( ( resolve , reject ) => {
87+ client . waitForReady ( Number . POSITIVE_INFINITY , ( err ) => {
88+ if ( err ) {
89+ reject ( err ) ;
90+ } else {
91+ resolve ( ) ;
92+ }
93+ } ) ;
94+ } ) ;
95+ }
96+
8097 public get minutesPerBlock ( ) {
8198 return LndClient . MINUTES_PER_BLOCK_BY_CURRENCY [ this . currency ] ;
8299 }
83100
84- /** Initializes the client for calls to lnd and verifies that we can connect to it. */
85- public init = async ( ) => {
101+ /**
102+ * Initializes the client for calls to lnd and verifies that we can connect to it.
103+ * @param awaitingCreate whether xud is waiting for its node key to be created
104+ */
105+ public init = async ( awaitingCreate = false ) => {
86106 assert ( this . lockBuffer > 0 , `lnd-${ this . currency } : lock buffer must be a positive number` ) ;
87107
88108 const { disable, certpath, macaroonpath, nomacaroons, host, port } = this . config ;
@@ -94,24 +114,28 @@ class LndClient extends SwapClient {
94114 try {
95115 const lndCert = await fs . readFile ( certpath ) ;
96116 this . credentials = grpc . credentials . createSsl ( lndCert ) ;
117+ this . logger . debug ( `loaded tls cert from ${ certpath } ` ) ;
97118 } catch ( err ) {
98- this . logger . error ( ' could not load lnd certificate , is lnd installed?' ) ;
119+ this . logger . error ( ` could not load tls cert from ${ certpath } , is lnd installed?` ) ;
99120 await this . setStatus ( ClientStatus . Disabled ) ;
100121 return ;
101122 }
102123
103- this . meta = new grpc . Metadata ( ) ;
104124 if ( ! nomacaroons ) {
125+ this . macaroonpath = macaroonpath ;
105126 try {
106- const adminMacaroon = await fs . readFile ( macaroonpath ) ;
107- this . meta . add ( 'macaroon' , adminMacaroon . toString ( 'hex' ) ) ;
127+ await this . loadMacaroon ( ) ;
108128 } catch ( err ) {
109- this . logger . error ( 'could not load lnd macaroon, is lnd installed?' ) ;
110- await this . setStatus ( ClientStatus . Disabled ) ;
111- return ;
129+ if ( ! awaitingCreate ) {
130+ // unless we are waiting for the xud nodekey and lnd wallet to be created
131+ // we expect the macaroon to exist and disable this client otherwise
132+ this . logger . error ( `expected macaroon not found at ${ macaroonpath } ` ) ;
133+ await this . setStatus ( ClientStatus . Disabled ) ;
134+ return ;
135+ }
112136 }
113137 } else {
114- this . logger . info ( 'macaroons are disabled for lnd ' ) ;
138+ this . logger . info ( 'macaroons are disabled' ) ;
115139 }
116140
117141 this . uri = `${ host } :${ port } ` ;
@@ -159,6 +183,14 @@ class LndClient extends SwapClient {
159183 } ) ;
160184 }
161185
186+ private loadMacaroon = async ( ) => {
187+ if ( this . macaroonpath ) {
188+ const adminMacaroon = await fs . readFile ( this . macaroonpath ) ;
189+ this . meta . add ( 'macaroon' , adminMacaroon . toString ( 'hex' ) ) ;
190+ this . logger . debug ( `loaded macaroon from ${ this . macaroonpath } ` ) ;
191+ }
192+ }
193+
162194 private unaryInvoiceCall = < T , U > ( methodName : string , params : T ) : Promise < U > => {
163195 return new Promise ( ( resolve , reject ) => {
164196 if ( this . isDisabled ( ) ) {
@@ -232,27 +264,87 @@ class LndClient extends SwapClient {
232264 } ;
233265 }
234266
267+ /**
268+ * Waits for the lnd wallet to be initialized and for its macaroons to be created then attempts
269+ * to verify the connection to lnd.
270+ */
271+ private awaitWalletInit = async ( ) => {
272+ // we are waiting for lnd to be initialized by xud and for the lnd macaroons to be created
273+ this . logger . info ( 'waiting for wallet to be initialized...' ) ;
274+
275+ const isWalletInitialized = await new Promise < boolean > ( ( resolve ) => {
276+ this . initWalletResolve = resolve ;
277+ } ) ;
278+
279+ if ( isWalletInitialized ) {
280+ if ( this . walletUnlocker ) {
281+ this . walletUnlocker . close ( ) ;
282+ this . walletUnlocker = undefined ;
283+ }
284+
285+ // admin.macaroon will not necessarily be created by the time lnd responds to a successful
286+ // InitWallet call, so we watch the folder that we expect it to be in for it to be created
287+ const watchMacaroonPromise = new Promise < boolean > ( ( resolve ) => {
288+ this . watchMacaroonResolve = resolve ;
289+ } ) ;
290+ const macaroonDir = path . join ( this . macaroonpath ! , '..' ) ;
291+ const fsWatcher = watch ( macaroonDir , ( _ , filename ) => {
292+ if ( filename === 'admin.macaroon' ) {
293+ this . logger . debug ( 'admin.macaroon was created' ) ;
294+ if ( this . watchMacaroonResolve ) {
295+ this . watchMacaroonResolve ( true ) ;
296+ }
297+ }
298+ } ) ;
299+ this . logger . debug ( `watching ${ macaroonDir } for admin.macaroon to be created` ) ;
300+ const macaroonCreated = await watchMacaroonPromise ;
301+ fsWatcher . close ( ) ;
302+ this . watchMacaroonResolve = undefined ;
303+
304+ if ( macaroonCreated ) {
305+ try {
306+ await this . loadMacaroon ( ) ;
307+
308+ // once we've loaded the macaroon we can attempt to verify the conneciton
309+ this . verifyConnection ( ) . catch ( this . logger . error ) ;
310+ } catch ( err ) {
311+ this . logger . error ( `could not load macaroon from ${ this . macaroonpath } ` ) ;
312+ await this . setStatus ( ClientStatus . Disabled ) ;
313+ }
314+ }
315+ }
316+ }
317+
235318 protected verifyConnection = async ( ) => {
236319 if ( this . isDisabled ( ) ) {
237320 throw ( errors . LND_IS_DISABLED ) ;
238321 }
239322
323+ if ( this . macaroonpath && this . meta . get ( 'macaroon' ) . length === 0 ) {
324+ // we have not loaded the macaroon yet - it is not created until the lnd wallet is initialized
325+ if ( ! this . isWaitingUnlock ( ) ) { // check that we are not already waiting for wallet init & unlock
326+ this . walletUnlocker = new WalletUnlockerClient ( this . uri , this . credentials ) ;
327+ await LndClient . waitForClientReady ( this . walletUnlocker ) ;
328+ await this . setStatus ( ClientStatus . WaitingUnlock ) ;
329+
330+ if ( this . reconnectionTimer ) {
331+ // we don't need scheduled attempts to retry the connection while waiting on the wallet
332+ clearTimeout ( this . reconnectionTimer ) ;
333+ this . reconnectionTimer = undefined ;
334+ }
335+
336+ this . awaitWalletInit ( ) . catch ( this . logger . error ) ;
337+ }
338+ return ;
339+ }
340+
240341 if ( ! this . isConnected ( ) ) {
241342 this . logger . info ( `trying to verify connection to lnd at ${ this . uri } ` ) ;
242- const lightningClient = new LightningClient ( this . uri , this . credentials ) ;
243- const clientReadyPromise = new Promise ( ( resolve , reject ) => {
244- lightningClient . waitForReady ( Number . POSITIVE_INFINITY , ( err ) => {
245- if ( err ) {
246- reject ( err ) ;
247- } else {
248- resolve ( ) ;
249- }
250- } ) ;
251- } ) ;
343+ this . lightning = new LightningClient ( this . uri , this . credentials ) ;
252344
253- this . lightning = lightningClient ;
254345 try {
255- await clientReadyPromise ;
346+ await LndClient . waitForClientReady ( this . lightning ) ;
347+
256348 const getInfoResponse = await this . getInfo ( ) ;
257349 if ( getInfoResponse . getSyncedToChain ( ) ) {
258350 // mark connection as active
@@ -585,19 +677,22 @@ class LndClient extends SwapClient {
585677 public initWallet = async ( walletPassword : string , seedMnemonic : string [ ] ) : Promise < lndrpc . InitWalletResponse . AsObject > => {
586678 const request = new lndrpc . InitWalletRequest ( ) ;
587679 request . setCipherSeedMnemonicList ( seedMnemonic ) ;
588- request . setWalletPassword ( walletPassword ) ;
680+ request . setWalletPassword ( Uint8Array . from ( Buffer . from ( walletPassword , 'utf8' ) ) ) ;
589681 const initWalletResponse = await this . unaryWalletUnlockerCall < lndrpc . InitWalletRequest , lndrpc . InitWalletResponse > (
590- 'initWallet' , new lndrpc . InitWalletRequest ( ) ,
682+ 'initWallet' , request ,
591683 ) ;
684+ if ( this . initWalletResolve ) {
685+ this . initWalletResolve ( true ) ;
686+ }
592687 this . logger . info ( 'wallet initialized' ) ;
593688 return initWalletResponse . toObject ( ) ;
594689 }
595690
596691 public unlockWallet = async ( walletPassword : string ) : Promise < lndrpc . UnlockWalletResponse . AsObject > => {
597692 const request = new lndrpc . UnlockWalletRequest ( ) ;
598- request . setWalletPassword ( walletPassword ) ;
693+ request . setWalletPassword ( Uint8Array . from ( Buffer . from ( walletPassword , 'utf8' ) ) ) ;
599694 const unlockWalletResponse = await this . unaryWalletUnlockerCall < lndrpc . UnlockWalletRequest , lndrpc . UnlockWalletResponse > (
600- 'unlockWallet' , new lndrpc . UnlockWalletRequest ( ) ,
695+ 'unlockWallet' , request ,
601696 ) ;
602697 this . logger . info ( 'wallet unlocked' ) ;
603698 return unlockWalletResponse . toObject ( ) ;
@@ -730,7 +825,18 @@ class LndClient extends SwapClient {
730825 this . invoices . close ( ) ;
731826 this . invoices = undefined ;
732827 }
733- await this . setStatus ( ClientStatus . Disconnected ) ;
828+ if ( this . initWalletResolve ) {
829+ this . initWalletResolve ( false ) ;
830+ this . initWalletResolve = undefined ;
831+ }
832+ if ( this . watchMacaroonResolve ) {
833+ this . watchMacaroonResolve ( false ) ;
834+ this . watchMacaroonResolve = undefined ;
835+ }
836+
837+ if ( ! this . isDisabled ( ) ) {
838+ await this . setStatus ( ClientStatus . Disconnected ) ;
839+ }
734840 }
735841}
736842
0 commit comments