import { MessagingService } from "@connext/messaging" ; import { AppRegistry , IChannelProvider , ILoggerService , IMessagingService , INodeApiClient , NATS_TIMEOUT , NodeResponses , StringMapping , ChannelMethods , NodeInitializationParameters , AsyncNodeInitializationParameters , VerifyNonceDtoType , Address , ConditionalTransferTypes , } from "@connext/types" ; import { bigNumberifyJson , isNode , logTime , stringify } from "@connext/utils" ; import axios , { AxiosResponse } from "axios" ; import { utils , providers , BigNumberish , BigNumber } from "ethers" ; import { v4 as uuid } from "uuid" ; import { createCFChannelProvider } from "./channelProvider" ; const { getAddress } = utils ; export class NodeApiClient implements INodeApiClient { public static async getBearerToken ( nodeUrl , userIdentifier , getSignature ) { const nonceResponse = await axios . get ( `${ nodeUrl }/auth/${ userIdentifier }` ) ; const nonce = nonceResponse . data ; const sig = await getSignature ( nonce ) ; const verifyResponse = await axios . post ( `${ nodeUrl }/auth` , { sig , userIdentifier , } as VerifyNonceDtoType ) ; return verifyResponse . data ; } public static async init ( opts ) { let getSignature ; let userIdentifier ; let messaging ; let channelProvider ; const { ethProvider , channelProvider : providedChannelProvider , signer , logger , messaging : providedMessaging , chainId , middlewareMap , } = opts ; const skipSync = typeof opts . skipSync === "boolean" ? opts . skipSync : true ; const log = logger . newContext ( "NodeApiClient" ) ; const nodeUrl = opts . nodeUrl ; const nodeHost = nodeUrl . replace ( /^.*:\/\// , "" ) . replace ( /\/.*/ , "" ) ; const messagingUrl = opts . messagingUrl || ( isNode ( ) ? `nats://${ nodeHost . replace ( /:[0-9]+$/ , "" ) }:4222` : nodeUrl . startsWith ( "https://" ) ? `wss://${ nodeHost }/api/messaging` : `ws://${ nodeHost }/api/messaging` ) ; if ( ! opts . messagingUrl ) { log . info ( `No messagingUrl provided, using ${ messagingUrl } derived from nodeUrl ${ nodeUrl }` ) ; } else { log . debug ( `Initializing messaging with nodeUrl ${ nodeUrl } & messagingUrl ${ messagingUrl }` ) ; } if ( signer ) { getSignature = ( msg ) => signer . signMessage ( msg ) ; userIdentifier = signer . publicIdentifier ; } else if ( providedChannelProvider ) { getSignature = async ( message ) => providedChannelProvider . send ( ChannelMethods . chan_signMessage , { message } ) ; userIdentifier = providedChannelProvider . config . userIdentifier ; } else { throw new Error ( "Must provide channelProvider or signer" ) ; } if ( ! providedMessaging ) { messaging = new MessagingService ( { messagingUrl , logger : log . newContext ( "Messaging" ) , } , "INDRA" , ( ) => NodeApiClient . getBearerToken ( nodeUrl , userIdentifier , getSignature ) ) ; } else { messaging = providedMessaging ; } await messaging . connect ( ) ; const node = new NodeApiClient ( { ... opts , nodeUrl , messaging } ) ; const config = await node . getConfig ( ) ; if ( config . ethNetwork . chainId !== chainId ) { throw new Error ( `Node config does not include info for ${ chainId }, only for ${ config . ethNetwork . chainId }` ) ; } node . userIdentifier = userIdentifier ; if ( ! providedChannelProvider ) { if ( config . nodeIdentifier === signer . publicIdentifier ) { throw new Error ( "Client must be instantiated with a signer that is different from the node's" ) ; } if ( ! opts . store ) { "Client must be instanatied with a store if no channelProvider is available" ; } channelProvider = await createCFChannelProvider ( { ethProvider , signer , node , middlewareMap , logger : log , store : opts . store , skipSync , } ) ; log . debug ( `Using channelProvider config: ${ stringify ( channelProvider . config ) }` ) ; node . channelProvider = channelProvider ; } return node ; } public chainId : number ; public nodeUrl : string ; public messaging : IMessagingService ; public latestSwapRates : StringMapping = { } ; public log : ILoggerService ; private _userIdentifier : string | undefined ; private _nodeIdentifier : string | undefined ; private _config : NodeResponses . GetConfig | undefined ; private _channelProvider : IChannelProvider | undefined ; constructor ( opts ) { this . messaging = opts . messaging ; this . log = opts . logger . newContext ( "NodeApiClient" ) ; this . _userIdentifier = opts . userIdentifier ; this . _nodeIdentifier = opts . nodeIdentifier ; this . _channelProvider = opts . channelProvider ; this . nodeUrl = opts . nodeUrl ; this . chainId = parseInt ( opts . chainId . toString ( ) , 10 ) ; } get userIdentifier ( ) : string | undefined { return this . _userIdentifier ; } set userIdentifier ( userAddress ) { this . _userIdentifier = userAddress ; } get nodeIdentifier ( ) : string | undefined { return this . _nodeIdentifier ; } set nodeIdentifier ( nodeAddress ) { this . _nodeIdentifier = nodeAddress ; } get config ( ) : NodeResponses . GetConfig | undefined { return this . _config ; } set config ( config ) { this . _config = config ; } get channelProvider ( ) : IChannelProvider | undefined { return this . _channelProvider ; } set channelProvider ( channelProvider ) { this . _channelProvider = channelProvider ; } async acquireLock ( lockName ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.lock.acquire.${ lockName }` ) ; } async releaseLock ( lockName , lockValue ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.lock.release.${ lockName }` , { lockValue , } ) ; } public async appRegistry ( ) { const response = await axios . get ( `${ this . nodeUrl }/app-registry/${ this . chainId }` ) ; return response . data ; } public async getConfig ( ) { const { data : config } = await axios . get ( `${ this . nodeUrl }/config/${ this . chainId }` ) ; this . config = config ; this . nodeIdentifier = config . nodeIdentifier ; return config ; } public async createChannel ( ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.channel.create` ) ; } public async getChannel ( ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.channel.get` ) ; } public async installPendingTransfers ( ) { const extendedTimeout = NATS_TIMEOUT * 5 ; return ( ( await this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.install-pending` , extendedTimeout ) ) || [ ] ) ; } public async getLatestSwapRate ( from , to ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.swap-rate.${ from }.${ to }.${ this . chainId }.${ this . chainId }` ) ; } public async getTransferHistory ( ) { return ( ( await this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.get-history` ) ) || [ ] ) ; } public async getHashLockTransfer ( lockHash , assetId ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.get-hashlock` , { lockHash , assetId , } ) ; } public async requestCollateral ( assetId , amount ? ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.channel.request-collateral` , { assetId , amount : amount ? BigNumber . from ( amount ) . toString ( ) : undefined , } ) ; } public async fetchLinkedTransfer ( paymentId ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.get-linked` , { paymentId , } ) ; } public async fetchSignedTransfer ( paymentId ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.get-signed` , { paymentId , } ) ; } public async fetchGraphTransfer ( paymentId ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.get-graph` , { paymentId , } ) ; } public async installConditionalTransferReceiverApp ( paymentId , conditionType ) { this . log . info ( `Start installConditionalTransferReceiverApp for ${ paymentId }: ${ conditionType }` ) ; const extendedTimeout = NATS_TIMEOUT * 5 ; return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.install-receiver` , { paymentId , conditionType , } , extendedTimeout ) ; } public async resolveLinkedTransfer ( paymentId ) { const extendedTimeout = NATS_TIMEOUT * 5 ; return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.install-linked` , { paymentId , } , extendedTimeout ) ; } public async resolveSignedTransfer ( paymentId ) { const extendedTimeout = NATS_TIMEOUT * 5 ; return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.transfer.install-signed` , { paymentId , } , extendedTimeout ) ; } public async getRebalanceProfile ( assetId ? ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.channel.get-profile` , { assetId : getAddress ( assetId ) , } ) ; } public async subscribeToSwapRates ( from , to , callback ) { await this . messaging . subscribe ( `swap-rate.${ from }.${ to }` , callback ) ; } public async unsubscribeFromSwapRates ( from , to ) { await this . messaging . unsubscribe ( `swap-rate.${ from }.${ to }` ) ; } public async restoreState ( ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.channel.restore` ) ; } public async getLatestWithdrawal ( ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.channel.latestWithdrawal` ) ; } public async clientCheckIn ( ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.client.check-in` ) ; } public async cancelChallenge ( appIdentityHash , signature ) { return this . send ( `${ this . userIdentifier }.${ this . nodeIdentifier }.${ this . chainId }.challenge.cancel` , { signature , appIdentityHash } ) ; } private async send ( subject , data ? , timeout = NATS_TIMEOUT ) { const start = Date . now ( ) ; const payload = { ... data , id : uuid ( ) , } ; let msg ; try { msg = await this . messaging . request ( subject , timeout , payload ) ; } catch ( e ) { throw new Error ( `Failed to send message: ${ e . message || e }` ) ; } const parsedData = typeof msg . data === "string" ? JSON . parse ( msg . data ) : msg . data ; const error = msg ? parsedData ? parsedData . response ? parsedData . response . err : "" : "" : "" ; if ( ! parsedData ) { this . log . info ( `Could not parse data, is this message malformed? ${ stringify ( msg ) }` ) ; return undefined ; } const { err , response } = parsedData ; if ( err || error || parsedData . err ) { throw new Error ( `Error sending request to subject ${ subject }. Message: ${ stringify ( msg ) }` ) ; } const isEmptyObj = typeof response === "object" && Object . keys ( response ) . length === 0 ; logTime ( this . log , start , `Node responded to ${ subject } request` ) ; return ! response || isEmptyObj ? undefined : bigNumberifyJson ( response ) ; } }