import { RippleAPI } from 'ripple-lib' ; import BigNumber from 'bignumber.js' ; import { CustomError } from '../../constants/errors' ; import { MESSAGES , RESPONSES } from '../../constants' ; import * as MessageTypes from '../../types/messages' ; import { Message , Response , SubscriptionAccountInfo , AccountInfo } from '../../types' ; import * as utils from './utils' ; import WorkerCommon from '../common' ; declare function postMessage ( data ) ; const common = new WorkerCommon ( postMessage ) ; let rippleApi ; let pingTimeout ; const DEFAULT_TIMEOUT = 20 * 1000 ; const DEFAULT_PING_TIMEOUT = 3 * 60 * 1000 ; let endpoints = [ ] ; const RESERVE = { BASE : '10000000' , OWNER : '2000000' , } ; const setPingTimeout = ( ) => { if ( pingTimeout ) { clearTimeout ( pingTimeout ) ; } pingTimeout = setTimeout ( onPing , common . getSettings ( ) . pingTimeout || DEFAULT_PING_TIMEOUT ) ; } ; const onPing = async ( ) => { if ( rippleApi && rippleApi . isConnected ( ) ) { if ( common . hasSubscriptions ( ) || common . getSettings ( ) . keepAlive ) { try { await rippleApi . getServerInfo ( ) ; } catch ( error ) { common . debug ( `Error in timeout ping request: ${ error }` ) ; } setPingTimeout ( ) ; } else { rippleApi . disconnect ( ) ; } } } ; const cleanup = ( ) => { if ( pingTimeout ) { clearTimeout ( pingTimeout ) ; } if ( rippleApi ) { rippleApi . removeAllListeners ( ) ; rippleApi = undefined ; } endpoints = [ ] ; common . removeAddresses ( common . getAddresses ( ) ) ; common . clearSubscriptions ( ) ; } ; const connect = async ( ) : Promise < RippleAPI > => { if ( rippleApi ) { if ( rippleApi . isConnected ( ) ) return rippleApi ; } const { server , timeout } = common . getSettings ( ) ; if ( ! server || ! Array . isArray ( server ) || server . length < 1 ) { throw new CustomError ( 'connect' , 'Endpoint not set' ) ; } if ( endpoints . length < 1 ) { endpoints = common . shuffleEndpoints ( server . slice ( 0 ) ) ; } common . debug ( 'Connecting to' , endpoints [ 0 ] ) ; let api ; try { api = new RippleAPI ( { server : endpoints [ 0 ] , timeout : timeout || DEFAULT_TIMEOUT , } ) ; api . connection . reconnect = ( ) => new Promise ( ( ) => { } ) ; await api . connect ( ) ; } catch ( error ) { common . debug ( 'Websocket connection failed' ) ; rippleApi = undefined ; endpoints . splice ( 0 , 1 ) ; if ( endpoints . length < 1 ) { throw new CustomError ( 'connect' , 'All backends are down' ) ; } return connect ( ) ; } api . on ( 'ledger' , ledger => { RESERVE . BASE = api . xrpToDrops ( ledger . reserveBaseXRP ) ; RESERVE . OWNER = api . xrpToDrops ( ledger . reserveIncrementXRP ) ; } ) ; api . on ( 'disconnected' , ( ) => { common . response ( { id : - 1 , type : RESPONSES . DISCONNECTED , payload : true } ) ; cleanup ( ) ; } ) ; common . response ( { id : - 1 , type : RESPONSES . CONNECTED } ) ; rippleApi = api ; return rippleApi ; } ; const getInfo = async ( data ) : Promise < void > => { try { const api = await connect ( ) ; const info = await api . getServerInfo ( ) ; RESERVE . BASE = api . xrpToDrops ( info . validatedLedger . reserveBaseXRP ) ; RESERVE . OWNER = api . xrpToDrops ( info . validatedLedger . reserveIncrementXRP ) ; common . response ( { id : data . id , type : RESPONSES . GET_INFO , payload : { url : api . connection . getUrl ( ) , ... utils . transformServerInfo ( info ) , } , } ) ; } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } finally { setPingTimeout ( ) ; } } ; const getMempoolAccountInfo = async ( account ) : Promise < { xrpBalance : string ; sequence : number ; txs : number ; } > => { const api = await connect ( ) ; const info = await api . request ( 'account_info' , { account , ledger_index : 'current' , queue : true , } ) ; return { xrpBalance : info . account_data . Balance , sequence : info . account_data . Sequence , txs : info . queue_data ? info . queue_data . txn_count : 0 , } ; } ; interface RawTxData { marker : { ledger : number ; seq : number ; } ; ledger_index_max : number ; limit : number ; transactions : any [ ] ; } const getRawTransactionsData = async ( options ) : Promise < RawTxData > => { const api = await connect ( ) ; return api . request ( 'account_tx' , options ) ; } ; const getAccountInfo = async ( data ) : Promise < void > => { const { payload } = data ; const account = { descriptor : payload . descriptor , balance : '0' , availableBalance : '0' , empty : true , history : { total : - 1 , unconfirmed : 0 , transactions : undefined , } , misc : { sequence : 0 , reserve : RESERVE . BASE , } , } ; try { const api = await connect ( ) ; const info = await api . getAccountInfo ( payload . descriptor ) ; const ownersReserve = info . ownerCount > 0 ? new BigNumber ( info . ownerCount ) . times ( RESERVE . OWNER ) . toString ( ) : '0' ; const reserve = new BigNumber ( RESERVE . BASE ) . plus ( ownersReserve ) . toString ( ) ; const misc = { sequence : info . sequence , reserve , } ; account . misc = misc ; account . balance = api . xrpToDrops ( info . xrpBalance ) ; account . availableBalance = new BigNumber ( account . balance ) . minus ( reserve ) . toString ( ) ; account . empty = false ; } catch ( error ) { if ( error . data && error . data . error === 'actNotFound' ) { common . response ( { id : data . id , type : RESPONSES . GET_ACCOUNT_INFO , payload : account , } ) ; } else { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } return ; } try { const mempoolInfo = await getMempoolAccountInfo ( payload . descriptor ) ; const { misc } = account ; const reserve = misc && typeof misc . reserve === 'string' ? misc . reserve : RESERVE . BASE ; account . availableBalance = new BigNumber ( mempoolInfo . xrpBalance ) . minus ( reserve ) . toString ( ) ; account . misc . sequence = mempoolInfo . sequence ; account . history . unconfirmed = mempoolInfo . txs ; } catch ( error ) { } if ( payload . details !== 'txs' ) { common . response ( { id : data . id , type : RESPONSES . GET_ACCOUNT_INFO , payload : account , } ) ; return ; } try { const requestOptions = { account : payload . descriptor , ledger_index_min : payload . from ? payload . from : undefined , ledger_index_max : payload . to ? payload . to : undefined , limit : payload . pageSize || 25 , marker : payload . marker , } ; const transactionsData = await getRawTransactionsData ( requestOptions ) ; account . history . transactions = transactionsData . transactions . map ( raw => utils . transformTransaction ( payload . descriptor , raw . tx ) ) ; common . response ( { id : data . id , type : RESPONSES . GET_ACCOUNT_INFO , payload : { ... account , marker : transactionsData . marker , } , } ) ; } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } } ; const getTransaction = async ( data ) : Promise < void > => { const { payload } = data ; try { const api = await connect ( ) ; const tx = await api . getTransaction ( payload ) ; common . response ( { id : data . id , type : RESPONSES . GET_TRANSACTION , payload : { type : 'ripple' , tx , } , } ) ; } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } finally { setPingTimeout ( ) ; } } ; const estimateFee = async ( data ) : Promise < void > => { try { const api = await connect ( ) ; const fee = await api . getFee ( ) ; let drops = api . xrpToDrops ( fee ) ; if ( new BigNumber ( drops ) . gt ( '2000' ) ) { drops = '12' ; } const payload = data . payload && Array . isArray ( data . payload . blocks ) ? data . payload . blocks . map ( ( ) => ( { feePerUnit : drops } ) ) : [ { feePerUnit : drops } ] ; common . response ( { id : data . id , type : RESPONSES . ESTIMATE_FEE , payload , } ) ; } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } finally { setPingTimeout ( ) ; } } ; const pushTransaction = async ( data ) : Promise < void > => { try { const api = await connect ( ) ; const info = await api . submit ( data . payload . toUpperCase ( ) ) ; if ( info . resultCode === 'tesSUCCESS' ) { common . response ( { id : data . id , type : RESPONSES . PUSH_TRANSACTION , payload : info . tx_json . hash , } ) ; } else { common . errorHandler ( { id : data . id , error : { message : info . resultMessage } } ) ; return ; } } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } finally { setPingTimeout ( ) ; } } ; const onNewBlock = ( event ) => { setPingTimeout ( ) ; common . response ( { id : - 1 , type : RESPONSES . NOTIFICATION , payload : { type : 'block' , payload : { blockHeight : event . ledgerVersion , blockHash : event . ledgerHash , } , } , } ) ; } ; const onTransaction = ( event ) => { setPingTimeout ( ) ; if ( event . type !== 'transaction' ) return ; const tx = event . transaction ; if ( event . transaction . TransactionType !== 'Payment' ) return ; const notify = ( descriptor ) => { common . response ( { id : - 1 , type : RESPONSES . NOTIFICATION , payload : { type : 'notification' , payload : { descriptor , tx : utils . transformTransaction ( descriptor , { ... event , ... tx } ) , } , } , } ) ; } ; const subscribed = common . getAddresses ( ) ; const sent = subscribed . find ( a => a === tx . Account ) ; if ( sent ) notify ( sent ) ; const recv = subscribed . find ( a => a === tx . Destination ) ; if ( recv ) notify ( recv ) ; } ; const subscribeAccounts = async ( accounts ) => { const api = await connect ( ) ; const prevAddresses = common . getAddresses ( ) ; common . addAccounts ( accounts ) ; const uniqueAddresses = common . getAddresses ( ) . filter ( a => prevAddresses . indexOf ( a ) < 0 ) ; if ( uniqueAddresses . length > 0 ) { if ( ! common . getSubscription ( 'notification' ) ) { api . connection . on ( 'transaction' , onTransaction ) ; common . addSubscription ( 'notification' ) ; } await api . request ( 'subscribe' , { accounts_proposed : uniqueAddresses , } ) ; } return { subscribed : common . getAddresses ( ) . length > 0 } ; } ; const subscribeAddresses = async ( addresses ) => { const api = await connect ( ) ; const uniqueAddresses = common . addAddresses ( addresses ) ; if ( uniqueAddresses . length > 0 ) { if ( ! common . getSubscription ( 'transaction' ) ) { api . connection . on ( 'transaction' , onTransaction ) ; common . addSubscription ( 'transaction' ) ; } const request = { accounts_proposed : uniqueAddresses , } ; await api . request ( 'subscribe' , request ) ; } return { subscribed : common . getAddresses ( ) . length > 0 } ; } ; const subscribeBlock = async ( ) => { if ( ! common . getSubscription ( 'ledger' ) ) { const api = await connect ( ) ; api . on ( 'ledger' , onNewBlock ) ; common . addSubscription ( 'ledger' ) ; } return { subscribed : true } ; } ; const subscribe = async ( data ) : Promise < void > => { const { payload } = data ; try { let response ; if ( payload . type === 'accounts' ) { response = await subscribeAccounts ( payload . accounts ) ; } else if ( payload . type === 'addresses' ) { response = await subscribeAddresses ( payload . addresses ) ; } else if ( payload . type === 'block' ) { response = await subscribeBlock ( ) ; } else { throw new CustomError ( 'invalid_param' , '+type' ) ; } common . response ( { id : data . id , type : RESPONSES . SUBSCRIBE , payload : response , } ) ; } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } finally { setPingTimeout ( ) ; } } ; const unsubscribeAddresses = async ( addresses ? ) => { const api = await connect ( ) ; if ( ! addresses ) { const all = common . getAddresses ( ) ; common . removeAccounts ( common . getAccounts ( ) ) ; common . removeAddresses ( all ) ; await api . request ( 'unsubscribe' , { accounts_proposed : all , } ) ; } else { common . removeAddresses ( addresses ) ; await api . request ( 'unsubscribe' , { accounts_proposed : addresses , } ) ; } if ( common . getAccounts ( ) . length < 1 ) { api . connection . removeListener ( 'transaction' , onTransaction ) ; common . removeSubscription ( 'transaction' ) ; } } ; const unsubscribeAccounts = async ( accounts ? ) => { const prevAddresses = common . getAddresses ( ) ; common . removeAccounts ( accounts || common . getAccounts ( ) ) ; const addresses = common . getAddresses ( ) ; const uniqueAddresses = prevAddresses . filter ( a => addresses . indexOf ( a ) < 0 ) ; await unsubscribeAddresses ( uniqueAddresses ) ; } ; const unsubscribeBlock = async ( ) => { if ( ! common . getSubscription ( 'ledger' ) ) return ; const api = await connect ( ) ; api . removeListener ( 'ledger' , onNewBlock ) ; common . removeSubscription ( 'ledger' ) ; } ; const unsubscribe = async ( data ) : Promise < void > => { const { payload } = data ; try { if ( payload . type === 'accounts' ) { await unsubscribeAccounts ( payload . accounts ) ; } else if ( payload . type === 'addresses' ) { await unsubscribeAddresses ( payload . addresses ) ; } else if ( payload . type === 'block' ) { await unsubscribeBlock ( ) ; } } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; return ; } finally { setPingTimeout ( ) ; } common . response ( { id : data . id , type : RESPONSES . UNSUBSCRIBE , payload : { subscribed : common . getAddresses ( ) . length > 0 } , } ) ; } ; const disconnect = async ( data ) => { if ( ! rippleApi ) { common . response ( { id : data . id , type : RESPONSES . DISCONNECTED , payload : true } ) ; return ; } try { await rippleApi . disconnect ( ) ; common . response ( { id : data . id , type : RESPONSES . DISCONNECTED , payload : true } ) ; } catch ( error ) { common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ; } } ; onmessage = ( event ) => { if ( ! event . data ) return ; const { data } = event ; common . debug ( 'onmessage' , data ) ; switch ( data . type ) { case MESSAGES . HANDSHAKE : common . setSettings ( data . settings ) ; break ; case MESSAGES . CONNECT : connect ( ) . then ( ( ) => { common . response ( { id : data . id , type : RESPONSES . CONNECT , payload : true } ) ; } ) . catch ( error => common . errorHandler ( { id : data . id , error : utils . transformError ( error ) } ) ) ; break ; case MESSAGES . GET_INFO : getInfo ( data ) ; break ; case MESSAGES . GET_ACCOUNT_INFO : getAccountInfo ( data ) ; break ; case MESSAGES . GET_TRANSACTION : getTransaction ( data ) ; break ; case MESSAGES . ESTIMATE_FEE : estimateFee ( data ) ; break ; case MESSAGES . PUSH_TRANSACTION : pushTransaction ( data ) ; break ; case MESSAGES . SUBSCRIBE : subscribe ( data ) ; break ; case MESSAGES . UNSUBSCRIBE : unsubscribe ( data ) ; break ; case MESSAGES . DISCONNECT : disconnect ( data ) ; break ; case 'terminate' : cleanup ( ) ; break ; default : common . errorHandler ( { id : data . id , error : new CustomError ( 'worker_unknown_request' , `+${ data . type }` ) , } ) ; break ; } } ; common . handshake ( ) ;