import { EventEmitter } from "events" ; import { OrderedMap } from "immutable" ; import { RenVMProvider } from "@renproject/provider" ; import { Chain , ChainTransactionProgress , DefaultTxWaiter , ErrorWithCode , EventEmitterTyped , generateGHash , generatePHash , generateSHash , InputChainTransaction , InputType , isContractChain , isDepositChain , OutputType , RenJSError , TxSubmitter , TxWaiter , utils , } from "@renproject/utils" ; import { defaultRenJSConfig , RenJSConfig } from "./config" ; import { estimateTransactionFee , GatewayFees } from "./fees" ; import { GatewayTransaction , TransactionParams } from "./gatewayTransaction" ; import { GatewayParams } from "./params" ; import { getInputAndOutputTypes } from "./utils/inputAndOutputTypes" ; class TransactionEmitter < ToPayload extends { chain : string ; txConfig ? : any ; } = any > extends EventEmitter implements EventEmitterTyped < { transaction : [ GatewayTransaction < ToPayload > ] ; } > { private getTransactions : ( ) => GatewayTransaction < ToPayload > [ ] ; public constructor ( getTransactions ) { super ( ) ; this . getTransactions = getTransactions ; } public addListener = < Event extends "transaction" > ( event , callback ) : this => { if ( event === "transaction" ) { this . getTransactions ( ) . map ( callback ) ; } super . on ( event , callback ) ; return this ; } ; category public on = < Event extends "transaction" > ( event , callback ) : this => this . addListener ( event , callback ) ; } noInheritDoc export class Gateway < FromPayload extends { chain : string ; txConfig ? : any ; } = any , ToPayload extends { chain : string ; txConfig ? : any ; } = any > { /** The parameters passed in when creating the LockAndMint. */ public readonly params : GatewayParams < FromPayload , ToPayload > ; public readonly fromChain : Chain ; public readonly toChain : Chain ; public readonly provider : RenVMProvider ; public readonly config : typeof defaultRenJSConfig & RenJSConfig ; /**
     * The generated gateway address for the lock-chain.
     */ public gatewayAddress : string | undefined ; public inSetup : { [ key ] : TxSubmitter | TxWaiter ; } = { } ; public in : TxSubmitter < ChainTransactionProgress , FromPayload [ "txConfig" ] > | TxWaiter | undefined ; /**
     * Deposits represents the lock deposits that have been detected so far.
     */ public transactions : OrderedMap < string , GatewayTransaction < ToPayload > | Promise < GatewayTransaction < ToPayload > > > = OrderedMap < string , GatewayTransaction < ToPayload > | Promise < GatewayTransaction < ToPayload > > > ( ) ; public eventEmitter : TransactionEmitter < ToPayload > = new TransactionEmitter < ToPayload > ( ( ) => this . transactions . filter ( ( tx ) => ( tx as any ) . then === undefined ) . map ( ( tx ) => tx as GatewayTransaction < ToPayload > ) . valueSeq ( ) . toArray ( ) ) ; private _selector : string | undefined ; public get selector ( ) : string { return this . _defaultGetter ( "_selector" ) || this . _selector ; } private _gHash : Buffer | undefined ; public get gHash ( ) : Buffer { return this . _defaultGetter ( "_gHash" ) || this . _gHash ; } private _pHash : Buffer | undefined ; public get pHash ( ) : Buffer { return this . _defaultGetter ( "_pHash" ) || this . _pHash ; } private _fees : GatewayFees | undefined ; public get fees ( ) : GatewayFees { return this . _defaultGetter ( "_fees" ) || this . _fees ; } private _outputType : OutputType | undefined ; public get outputType ( ) : OutputType { return this . _defaultGetter ( "_outputType" ) || this . _outputType ; } private _inputType : InputType | undefined ; public get inputType ( ) : InputType { return this . _defaultGetter ( "_inputType" ) || this . _inputType ; } private _inConfirmationTarget : number | undefined ; public get inConfirmationTarget ( ) : number { return ( this . _defaultGetter ( "_inConfirmationTarget" ) || this . _inConfirmationTarget ) ; } hidden public constructor ( renVM , fromChain , toChain , params , config = { } ) { this . params = params ; this . fromChain = fromChain ; this . toChain = toChain ; this . provider = renVM ; this . config = { ... defaultRenJSConfig , ... config , } ; { this . config . logger . debug ( "gateway created:" , String ( renVM . endpointOrProvider ) , fromChain . chain , toChain . chain , this . params , config ) ; } } hidden public async initialize ( ) { const { to , asset , from } = this . params ; const { inputType , outputType , selector } = await getInputAndOutputTypes ( { asset , fromChain : this . fromChain , toChain : this . toChain , } ) ; this . _inputType = inputType ; this . _outputType = outputType ; this . _selector = selector ; if ( this . outputType === OutputType . Release && ! isContractChain ( this . fromChain ) ) { throw ErrorWithCode . from ( new Error ( `Cannot release from non-contract chain ${ this . fromChain . chain }` ) , RenJSError . PARAMETER_ERROR ) ; } if ( this . outputType === OutputType . Mint && ! isContractChain ( this . toChain ) ) { throw ErrorWithCode . from ( new Error ( `Cannot mint ${ asset } to non-contract chain ${ this . toChain . chain }` ) , RenJSError . PARAMETER_ERROR ) ; } const [ confirmationTarget , shard , payload ] = await Promise . all ( [ this . provider . getConfirmationTarget ( this . fromChain . chain ) , ( async ( ) => { if ( utils . isDefined ( this . params . shard ) ) { return this . params . shard ; } if ( this . inputType === InputType . Lock && isDepositChain ( this . fromChain ) && ( await this . fromChain . isDepositAsset ( asset ) ) ) { return await this . provider . selectShard ( this . params . asset ) ; } else { return { gPubKey : "" , } ; } } ) ( ) , this . toChain . getOutputPayload ( asset , this . outputType as any , to ) , ] ) ; this . params . shard = shard ; this . _inConfirmationTarget = confirmationTarget ; this . _pHash = generatePHash ( payload . payload ) ; const sHash = generateSHash ( `${ this . params . asset }/to${ this . params . to . chain }` ) ; if ( isDepositChain ( this . fromChain ) && ( await this . fromChain . isDepositAsset ( this . params . asset ) ) ) { try { if ( ! isContractChain ( this . toChain ) ) { throw new Error ( `Cannot mint ${ asset } to non-contract chain ${ this . toChain . chain }.` ) ; } if ( ! this . params . shard ) { throw ErrorWithCode . from ( new Error ( `RenVM shard not selected.` ) , RenJSError . INTERNAL_ERROR ) ; } const nonce = typeof this . params . nonce === "string" ? utils . fromBase64 ( this . params . nonce ) : utils . toNBytes ( this . params . nonce || 0 , 32 ) ; const gHash = generateGHash ( this . pHash , sHash , payload . toBytes , nonce ) ; this . _gHash = gHash ; const gPubKey = utils . fromBase64 ( this . params . shard . gPubKey ) ; this . config . logger . debug ( "gPubKey:" , utils . Ox ( gPubKey ) ) ; if ( ! gPubKey || gPubKey . length === 0 ) { throw ErrorWithCode . from ( new Error ( "Unable to fetch RenVM shard public key." ) , RenJSError . NETWORK_ERROR ) ; } if ( ! gHash || gHash . length === 0 ) { throw new Error ( "Invalid gateway hash being passed to gateway address generation." ) ; } if ( ! asset || asset . length === 0 ) { throw new Error ( "Invalid asset being passed to gateway address generation." ) ; } const gatewayAddress = await this . fromChain . createGatewayAddress ( this . params . asset , this . params . from , gPubKey , gHash ) ; this . gatewayAddress = gatewayAddress ; try { await utils . tryNTimes ( async ( ) => { await this . provider . submitGatewayDetails ( gatewayAddress , { selector , payload : payload . payload , pHash : this . pHash , to : payload . to , nonce : nonce , nHash : Buffer . from ( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" , "base64" ) , gPubKey : gPubKey , gHash : gHash , } ) ; } , 5 ) ; } catch ( error ) { throw new ErrorWithCode ( `Error submitting gateway details: ${ utils . extractError ( error ) }` , RenJSError . GATEWAY_SUBMISSION_FAILED ) ; } this . config . logger . debug ( "gateway address:" , this . gatewayAddress ) ; } catch ( error ) { throw error ; } this . _watchForDeposits ( ) . catch ( console . error ) ; } if ( isContractChain ( this . fromChain ) ) { const processInput = ( input ) => { const nonce = utils . toURLBase64 ( input . nonce ? utils . fromBase64 ( input . nonce ) : typeof this . params . nonce === "string" ? utils . fromBase64 ( this . params . nonce ) : utils . toNBytes ( this . params . nonce || 0 , 32 ) ) ; const gHash = generateGHash ( payload . payload , sHash , payload . toBytes , utils . fromBase64 ( nonce ) ) ; this . _gHash = gHash ; if ( ! gHash || gHash . length === 0 ) { throw ErrorWithCode . from ( new Error ( "Invalid gateway hash being passed to gateway address generation." ) , RenJSError . PARAMETER_ERROR ) ; } if ( ! asset || asset . length === 0 ) { throw ErrorWithCode . from ( new Error ( "Invalid asset being passed to gateway address generation." ) , RenJSError . PARAMETER_ERROR ) ; } this . processDeposit ( input ) . catch ( console . error ) ; } ; const removeInput = ( ) => { } ; this . in = await this . fromChain . getInputTx ( this . inputType , asset , from , ( ) => ( { toChain : to . chain , toPayload : payload , gatewayAddress : this . gatewayAddress , } ) , this . inConfirmationTarget , processInput , removeInput ) ; } this . _fees = await estimateTransactionFee ( this . provider , asset , this . fromChain , this . toChain ) ; if ( isContractChain ( this . fromChain ) && this . fromChain . getInSetup ) { this . inSetup = { ... this . inSetup , ... ( await this . fromChain . getInSetup ( asset , this . inputType , from , ( ) => ( { toChain : to . chain , toPayload : payload , gatewayAddress : this . gatewayAddress , } ) ) ) , } ; } return this ; } param deposit category public async processDeposit ( deposit ) { const depositIdentifier = deposit . txid + "_" + String ( deposit . txindex ) ; const existingTransaction = this . transactions . get ( depositIdentifier ) ; if ( ! existingTransaction ) { const createGatewayTransaction = async ( ) => { if ( ! this . pHash || ! this . gHash ) { throw new Error ( "Gateway address must be generated before calling 'processDeposit'." ) ; } const nonce = utils . toURLBase64 ( deposit . nonce ? utils . fromBase64 ( deposit . nonce ) : typeof this . params . nonce === "string" ? utils . fromBase64 ( this . params . nonce ) : utils . toNBytes ( this . params . nonce || 0 , 32 ) ) ; const params = { asset : this . params . asset , fromTx : deposit , to : this . params . to , shard : this . params . shard , nonce , } ; let inTx = this . in ; if ( ! inTx ) { inTx = new DefaultTxWaiter ( { chainTransaction : deposit , chain : this . fromChain , target : this . inConfirmationTarget , } ) ; } const transaction = new GatewayTransaction < ToPayload > ( this . provider , this . fromChain , this . toChain , params , inTx , this . config ) ; await transaction . initialize ( ) ; this . eventEmitter . emit ( "transaction" , transaction ) ; this . config . logger . debug ( "new deposit:" , deposit ) ; return transaction ; } ; const promise = createGatewayTransaction ( ) ; this . transactions = this . transactions . set ( depositIdentifier , promise ) ; try { this . transactions = this . transactions . set ( depositIdentifier , await promise ) ; } catch ( error ) { this . transactions = this . transactions . remove ( depositIdentifier ) ; } } return ( await existingTransaction ) ! ; } public on < Event extends "transaction" > ( event , callback ) { this . eventEmitter . on ( event , callback ) ; return this ; } /** PRIVATE METHODS */ private _defaultGetter ( name ) { if ( this [ name ] === undefined ) { throw new Error ( `Must call 'initialize' before accessing '${ name }'.` ) ; } return this [ name ] ; } private async _watchForDeposits ( ) { if ( ! this . gatewayAddress || ! isDepositChain ( this . fromChain ) || ! this . fromChain . watchForDeposits ) { return ; } while ( true ) { const listenerCancelled = ( ) => this . eventEmitter . listenerCount ( "transaction" ) === 0 ; try { if ( listenerCancelled ( ) ) { await utils . sleep ( 1 * utils . sleep . SECONDS ) ; continue ; } } catch ( error ) { this . config . logger . error ( utils . extractError ( error ) ) ; } const onDeposit = ( deposit ) : void => { try { this . processDeposit ( deposit ) . catch ( console . error ) ; } catch ( error ) { this . config . logger . error ( error ) ; } } ; const cancelDeposit = ( ) => { } ; try { await this . fromChain . watchForDeposits ( this . params . asset , this . params . from , this . gatewayAddress , onDeposit , cancelDeposit , listenerCancelled ) ; } catch ( error ) { this . config . logger . error ( utils . extractError ( error ) ) ; } await utils . sleep ( this . config . networkDelay ) ; } } }