import { SubmittableResult } from '@polkadot/api/submittable' ; import { AddressOrPair , SubmittableExtrinsic , } from '@polkadot/api/submittable/types' ; import { KeyringPair } from '@polkadot/keyring/types' ; import { Balance } from '@polkadot/types/interfaces' ; import { logger } from '@polkadot/util' ; import BN from 'bn.js' ; import React , { createContext , useReducer } from 'react' ; import { Subject } from 'rxjs' ; import { ValueOf } from './types' ; const l = logger ( 'tx-queue' ) ; const INIT_ERROR = new Error ( 'TxQueueContext called without Provider.' ) ; type Extrinsic = SubmittableExtrinsic < 'rxjs' > ; export interface ExtrinsicDetails { allFees : BN ; allTotal : BN ; amount : Balance ; methodCall : string ; recipientAddress ? : string ; senderPair : AddressOrPair ; } function isSenderPairKeyring ( senderPair ) { if ( ( senderPair as KeyringPair ) . decodePkcs8 !== undefined ) { return true ; } } /**
 * An item from the TxQueue
 */ export interface PendingExtrinsic { details : ExtrinsicDetails ; extrinsic : Extrinsic ; id : number ; status : { isAskingForConfirm : boolean ; isFinalized : boolean ; isDropped : boolean ; isPending : boolean ; isUsurped : boolean ; } ; unsubscribe : ( ) => void ; } interface State { txCounter : number ; txQueue : PendingExtrinsic [ ] ; } const INITIAL_STATE = { txCounter : 0 , txQueue : [ ] , } ; enum ActionTypes { 'setTxCounter' , 'setTxQueue' } interface Action { type : keyof typeof ActionTypes ; data : ValueOf < State > ; } const stateReducer = ( state , action ) : State => { switch ( action . type ) { case 'setTxCounter' : return { ... state , txCounter : action . data as number } ; case 'setTxQueue' : return { ... state , txQueue : action . data as PendingExtrinsic [ ] } ; default : return state ; } } ; export interface EnqueueParams extends ExtrinsicDetails { extrinsic : Extrinsic ; } interface Props { children : React . ReactNode ; } const cancelObservable = new Subject < { msg : string ; } > ( ) ; const statusObservable = new Subject < { msg : string ; } > ( ) ; const successObservable = new Subject < ExtrinsicDetails > ( ) ; const errorObservable = new Subject < { error : string ; } > ( ) ; export const TxQueueContext = createContext ( { enqueue : ( extrinsic , details ) => { console . error ( INIT_ERROR ) ; return 0 as number ; } , signAndSubmit : ( extrinsicId ) => { console . error ( INIT_ERROR ) ; } , state : INITIAL_STATE , clear : ( ) => { console . error ( INIT_ERROR ) ; } , cancelObservable , statusObservable , successObservable , errorObservable , } ) ; export function TxQueueContextProvider ( props ) { const [ state , dispatch ] = useReducer ( stateReducer , INITIAL_STATE ) ; /**
     * Replace tx with id `extrinsicId` with a new tx
     */ const replaceTx = ( extrinsicId , newTx ) : void => { const { txQueue } = state ; const newTxQueue = txQueue . map ( ( tx ) => tx . id === extrinsicId ? newTx : tx ) ; dispatch ( { type : 'setTxQueue' , data : newTxQueue , } ) ; } ; /**
     * Clear the txQueue.
     */ const clear = ( ) : void => { const { txQueue } = state ; const msg = [ ] ; txQueue . forEach ( ( { extrinsic : { method } , unsubscribe } ) => { msg . push ( `${ method . sectionName }.${ method . methodName }` ) ; unsubscribe ( ) ; } ) ; dispatch ( { type : 'setTxQueue' , data : [ ] , } ) ; l . log ( 'Cleared all extrinsics' ) ; cancelObservable . next ( { msg : `cleared the following extrinsic(s): ${ msg . join ( ' ' ) }` , } ) ; } ; /**
     * Unsubscribe the tx with id `extrinsicId`
     */ const closeTxSubscription = ( extrinsicId ) : void => { const { txQueue } = state ; const tx = txQueue . find ( tx => tx . id === extrinsicId ) ; if ( tx ) { tx . unsubscribe ( ) ; dispatch ( { type : 'setTxQueue' , data : txQueue . filter ( tx => tx . id !== extrinsicId ) , } ) ; } } ; /**
     * Add a tx to the queue
     * return tx id
     */ const enqueue = ( extrinsic , details ) : number => { const { txCounter , txQueue } = state ; const extrinsicId = txCounter ; dispatch ( { type : 'setTxCounter' , data : txCounter + 1 , } ) ; l . log ( `Queued extrinsic #${ extrinsicId } from ${ isSenderPairKeyring ( details . senderPair ) ? ( details . senderPair as KeyringPair ) . address : ( details . senderPair as string ) } to ${ details . recipientAddress } of amount ${ details . amount }` , details ) ; dispatch ( { type : 'setTxQueue' , data : txQueue . concat ( { details , extrinsic , id : extrinsicId , status : { isAskingForConfirm : true , isFinalized : false , isDropped : false , isPending : false , isUsurped : false , } , unsubscribe : ( ) => { } , } ) , } ) ; return extrinsicId ; } ; /**
     * Sign and send the tx with id `extrinsicId`
     */ const signAndSubmit = ( extrinsicId ) : void => { const { txQueue } = state ; const pendingExtrinsic = txQueue . find ( tx => tx . id === extrinsicId ) ; if ( ! pendingExtrinsic ) { l . error ( `There's no extrinsic with id #${ extrinsicId }` ) ; return ; } const { details , extrinsic , status } = pendingExtrinsic ; const { senderPair } = details ; if ( ! status . isAskingForConfirm ) { l . error ( `Extrinsic #${ extrinsicId } is being submitted, but its status is not isAskingForConfirm` ) ; return ; } l . log ( `Extrinsic #${ extrinsicId } is being sent` ) ; const subscription = extrinsic . signAndSend ( senderPair ) . subscribe ( ( txResult ) => { console . log ( 'tx result received => ' , txResult ) ; const { status : { isFinalized , isDropped , isUsurped } , } = txResult ; l . log ( `Extrinsic #${ extrinsicId } has new status:` , txResult ) ; statusObservable . next ( { msg : `Extrinsic #${ extrinsicId } has new status: ${ txResult . status . toHuman ( ) }` , } ) ; replaceTx ( extrinsicId , { ... pendingExtrinsic , status : { isAskingForConfirm : false , isDropped , isFinalized , isPending : false , isUsurped , } , } ) ; if ( isFinalized ) { successObservable . next ( details ) ; } if ( isFinalized || isDropped || isUsurped ) { closeTxSubscription ( extrinsicId ) ; } } , ( error ) => { errorObservable . next ( { error : error . message } ) ; } , ( ) => { if ( isSenderPairKeyring ( senderPair ) ) { ( senderPair as KeyringPair ) . lock ( ) ; } } ) ; replaceTx ( extrinsicId , { ... pendingExtrinsic , status : { ... pendingExtrinsic . status , isAskingForConfirm : false , isPending : true , } , unsubscribe : ( ) => { subscription . unsubscribe ( ) ; } , } ) ; } ; return ( < TxQueueContext . Provider value = { { clear , enqueue , signAndSubmit , state , successObservable , statusObservable , errorObservable , cancelObservable , } } > { props . children } < /TxQueueContext.Provider>); }