import DolphinCoin from "@counterfactual/cf-funding-protocol-contracts/expected-build-artifacts/DolphinCoin.json" ; import { NetworkContextForTestSuite } from "@counterfactual/local-ganache-server" ; import { AppABIEncodings , AppInstanceJson , AppInstanceProposal , ContractABI , Node as NodeTypes , OutcomeType , SolidityValueType } from "@counterfactual/types" ; import { Contract , Wallet } from "ethers" ; import { One , Zero } from "ethers/constants" ; import { JsonRpcProvider } from "ethers/providers" ; import { BigNumber , bigNumberify } from "ethers/utils" ; import { CreateChannelMessage , InstallMessage , InstallVirtualMessage , jsonRpcDeserialize , JsonRpcResponse , Node , NODE_EVENTS , ProposeMessage , Rpc , UninstallMessage , UninstallVirtualMessage } from "../../src" ; import { CONVENTION_FOR_ETH_TOKEN_ADDRESS } from "../../src/constants" ; import { xkeyKthAddress } from "../../src/engine" ; import { initialLinkedState , linkedAbiEncodings } from "./linked-transfer" ; import { initialSimpleTransferState , simpleTransferAbiEncodings } from "./simple-transfer" ; import { initialEmptyTTTState , tttAbiEncodings } from "./tic-tac-toe" ; import { initialTransferState , transferAbiEncodings } from "./unidirectional-transfer" ; interface AppContext { appDefinition : string ; abiEncodings : AppABIEncodings ; initialState : SolidityValueType ; outcomeType : OutcomeType ; } const { TicTacToeApp , SimpleTransferApp , UnidirectionalLinkedTransferApp , UnidirectionalTransferApp } = global [ "networkContext" ] as NetworkContextForTestSuite ; /**
 * Even though this function returns a transaction hash, the calling Node
 * will receive an event (CREATE_CHANNEL) that should be subscribed to to
 * ensure a channel has been instantiated and to get its multisig address
 * back in the event data.
 */ export async function getMultisigCreationTransactionHash ( node , xpubs ) { const { result : { result : { transactionHash } } } = await node . rpcRouter . dispatch ( { id : Date . now ( ) , methodName : NodeTypes . RpcMethodName . CREATE_CHANNEL , parameters : { owners : xpubs } } ) ; return transactionHash ; } param node returns export async function getChannelAddresses ( node ) { const { result : { result : { multisigAddresses } } } = await node . rpcRouter . dispatch ( { id : Date . now ( ) , methodName : NodeTypes . RpcMethodName . GET_CHANNEL_ADDRESSES , parameters : { } } ) ; return new Set ( multisigAddresses ) ; } export async function getAppInstance ( node , appInstanceId ) { const { result : { result : { appInstance } } } = await node . rpcRouter . dispatch ( { id : Date . now ( ) , methodName : NodeTypes . RpcMethodName . GET_APP_INSTANCE_DETAILS , parameters : { appInstanceId } } ) ; return appInstance ; } export async function getAppInstanceProposal ( node , appInstanceId ) { const candidates = ( await getProposedAppInstances ( node ) ) . filter ( proposal => proposal . identityHash === appInstanceId ) ; if ( candidates . length === 0 ) { throw new Error ( "Could not find proposal" ) ; } if ( candidates . length > 1 ) { throw new Error ( "Failed to match exactly one proposed app instance" ) ; } return candidates [ 0 ] ; } export async function getFreeBalanceState ( node , multisigAddress , tokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const { result : { result } } = await node . rpcRouter . dispatch ( { id : Date . now ( ) , methodName : NodeTypes . RpcMethodName . GET_FREE_BALANCE_STATE , parameters : { multisigAddress , tokenAddress } } ) ; return result ; } export async function getTokenIndexedFreeBalanceStates ( node , multisigAddress ) { const { result : { result } } = await node . rpcRouter . dispatch ( { id : Date . now ( ) , methodName : NodeTypes . RpcMethodName . GET_TOKEN_INDEXED_FREE_BALANCE_STATES , parameters : { multisigAddress } } ) ; return result as NodeTypes . GetTokenIndexedFreeBalanceStatesResult ; } export async function getInstalledAppInstances ( node ) { const rpc = jsonRpcDeserialize ( { jsonrpc : "2.0" , id : Date . now ( ) , method : NodeTypes . RpcMethodName . GET_APP_INSTANCES , params : { } as NodeTypes . GetAppInstancesParams } ) ; const response = ( await node . rpcRouter . dispatch ( rpc ) ) as JsonRpcResponse ; const result = response . result . result as NodeTypes . GetAppInstancesResult ; return result . appInstances ; } export async function getProposedAppInstances ( node ) { const rpc = jsonRpcDeserialize ( { jsonrpc : "2.0" , id : Date . now ( ) , method : NodeTypes . RpcMethodName . GET_PROPOSED_APP_INSTANCES , params : { } as NodeTypes . GetProposedAppInstancesParams } ) ; const response = ( await node . rpcRouter . dispatch ( rpc ) ) as JsonRpcResponse ; const result = response . result . result as NodeTypes . GetProposedAppInstancesResult ; return result . appInstances ; } export async function deposit ( node , multisigAddress , amount = One , tokenAddress ? ) { const depositReq = constructDepositRpc ( multisigAddress , amount , tokenAddress ) ; await node . rpcRouter . dispatch ( depositReq ) ; } export async function deployStateDepositHolder ( node , multisigAddress ) { const response = await node . rpcRouter . dispatch ( { methodName : NodeTypes . RpcMethodName . DEPLOY_STATE_DEPOSIT_HOLDER , parameters : { multisigAddress } as NodeTypes . DeployStateDepositHolderParams } ) ; const result = response . result . result as NodeTypes . DeployStateDepositHolderResult ; return result . transactionHash ; } export function constructDepositRpc ( multisigAddress , amount , tokenAddress ? ) { return jsonRpcDeserialize ( { id : Date . now ( ) , method : NodeTypes . RpcMethodName . DEPOSIT , params : { multisigAddress , amount , tokenAddress } as NodeTypes . DepositParams , jsonrpc : "2.0" } ) ; } export function constructWithdrawCommitmentRpc ( multisigAddress , amount , tokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , recipient ? ) { const withdrawCommitmentReq = constructWithdrawRpc ( multisigAddress , amount , tokenAddress , recipient ) ; withdrawCommitmentReq . methodName = NodeTypes . RpcMethodName . WITHDRAW_COMMITMENT ; return withdrawCommitmentReq ; } export function constructWithdrawRpc ( multisigAddress , amount , tokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , recipient ? ) { return jsonRpcDeserialize ( { id : Date . now ( ) , method : NodeTypes . RpcMethodName . WITHDRAW , params : { tokenAddress , multisigAddress , amount , recipient } as NodeTypes . WithdrawParams , jsonrpc : "2.0" } ) ; } export function constructInstallRpc ( appInstanceId ) { return jsonRpcDeserialize ( { id : Date . now ( ) , method : NodeTypes . RpcMethodName . INSTALL , params : { appInstanceId } as NodeTypes . InstallParams , jsonrpc : "2.0" } ) ; } export function constructRejectInstallRpc ( appInstanceId ) { return jsonRpcDeserialize ( { id : Date . now ( ) , method : NodeTypes . RpcMethodName . REJECT_INSTALL , params : { appInstanceId } as NodeTypes . RejectInstallParams , jsonrpc : "2.0" } ) ; } export function constructAppProposalRpc ( proposedToIdentifier , appDefinition , abiEncodings , initialState , initiatorDeposit = Zero , initiatorDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , responderDeposit = Zero , responderDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const { outcomeType } = getAppContext ( appDefinition , initialState ) ; return jsonRpcDeserialize ( { id : Date . now ( ) , method : NodeTypes . RpcMethodName . PROPOSE_INSTALL , jsonrpc : "2.0" , params : { proposedToIdentifier , initiatorDeposit , initiatorDepositTokenAddress , responderDeposit , responderDepositTokenAddress , appDefinition , initialState , abiEncodings , outcomeType , timeout : One } as NodeTypes . ProposeInstallParams } ) ; } export function constructInstallVirtualRpc ( appInstanceId , intermediaryIdentifier ) { return jsonRpcDeserialize ( { params : { appInstanceId , intermediaryIdentifier } as NodeTypes . InstallVirtualParams , id : Date . now ( ) , method : NodeTypes . RpcMethodName . INSTALL_VIRTUAL , jsonrpc : "2.0" } ) ; } export function constructVirtualProposalRpc ( proposedToIdentifier , intermediaryIdentifier , appDefinition , abiEncodings , initialState = { } , initiatorDeposit = Zero , initiatorDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , responderDeposit = Zero , responderDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const installProposalParams = constructAppProposalRpc ( proposedToIdentifier , appDefinition , abiEncodings , initialState , initiatorDeposit , initiatorDepositTokenAddress , responderDeposit , responderDepositTokenAddress ) . parameters as NodeTypes . ProposeInstallParams ; const installVirtualParams = { ... installProposalParams , intermediaryIdentifier } ; return jsonRpcDeserialize ( { params : installVirtualParams , id : Date . now ( ) , method : NodeTypes . RpcMethodName . PROPOSE_INSTALL , jsonrpc : "2.0" } ) ; } param proposalParams param appInstanceProposal export function confirmProposedAppInstance ( methodParams , appInstanceProposal , nonInitiatingNode = false ) { const proposalParams = methodParams as NodeTypes . ProposeInstallParams ; expect ( proposalParams . abiEncodings ) . toEqual ( appInstanceProposal . abiEncodings ) ; expect ( proposalParams . appDefinition ) . toEqual ( appInstanceProposal . appDefinition ) ; if ( nonInitiatingNode ) { expect ( proposalParams . initiatorDeposit ) . toEqual ( bigNumberify ( appInstanceProposal . responderDeposit ) ) ; expect ( proposalParams . responderDeposit ) . toEqual ( bigNumberify ( appInstanceProposal . initiatorDeposit ) ) ; } else { expect ( proposalParams . initiatorDeposit ) . toEqual ( bigNumberify ( appInstanceProposal . initiatorDeposit ) ) ; expect ( proposalParams . responderDeposit ) . toEqual ( bigNumberify ( appInstanceProposal . responderDeposit ) ) ; } expect ( proposalParams . timeout ) . toEqual ( bigNumberify ( appInstanceProposal . timeout ) ) ; } export function constructGetStateRpc ( appInstanceId ) { return jsonRpcDeserialize ( { params : { appInstanceId } , id : Date . now ( ) , method : NodeTypes . RpcMethodName . GET_STATE , jsonrpc : "2.0" } ) ; } export function constructTakeActionRpc ( appInstanceId , action ) { return jsonRpcDeserialize ( { params : { appInstanceId , action } as NodeTypes . TakeActionParams , id : Date . now ( ) , jsonrpc : "2.0" , method : NodeTypes . RpcMethodName . TAKE_ACTION } ) ; } export function constructGetAppsRpc ( ) { return jsonRpcDeserialize ( { params : { } , id : Date . now ( ) , method : NodeTypes . RpcMethodName . GET_APP_INSTANCES , jsonrpc : "2.0" } ) ; } export function constructUninstallRpc ( appInstanceId ) { return jsonRpcDeserialize ( { params : { appInstanceId } as NodeTypes . UninstallParams , id : Date . now ( ) , jsonrpc : "2.0" , method : NodeTypes . RpcMethodName . UNINSTALL } ) ; } export function constructUninstallVirtualRpc ( appInstanceId , intermediaryIdentifier ) { return jsonRpcDeserialize ( { params : { appInstanceId , intermediaryIdentifier } as NodeTypes . UninstallVirtualParams , id : Date . now ( ) , jsonrpc : "2.0" , method : NodeTypes . RpcMethodName . UNINSTALL_VIRTUAL } ) ; } export async function collateralizeChannel ( multisigAddress , node1 , node2 ? , amount = One , tokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const depositReq = constructDepositRpc ( multisigAddress , amount , tokenAddress ) ; await node1 . rpcRouter . dispatch ( depositReq ) ; if ( ! node2 ) return ; await node2 . rpcRouter . dispatch ( depositReq ) ; } export async function createChannel ( nodeA , nodeB ) { return new Promise ( async ( resolve ) => { nodeB . on ( NODE_EVENTS . CREATE_CHANNEL , async ( msg ) => { expect ( await getInstalledAppInstances ( nodeB ) ) . toEqual ( [ ] ) ; resolve ( msg . data . multisigAddress ) ; } ) ; await getMultisigCreationTransactionHash ( nodeA , [ nodeA . publicIdentifier , nodeB . publicIdentifier ] ) ; expect ( await getInstalledAppInstances ( nodeA ) ) . toEqual ( [ ] ) ; } ) ; } export async function installApp ( nodeA , nodeB , appDefinition , initialState ? , initiatorDeposit = Zero , initiatorDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , responderDeposit = Zero , responderDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const appContext = getAppContext ( appDefinition , initialState ) ; const installationProposalRpc = constructAppProposalRpc ( nodeB . publicIdentifier , appContext . appDefinition , appContext . abiEncodings , appContext . initialState , initiatorDeposit , initiatorDepositTokenAddress , responderDeposit , responderDepositTokenAddress ) ; const proposedParams = installationProposalRpc . parameters as NodeTypes . ProposeInstallParams ; return new Promise ( async ( resolve ) => { nodeB . once ( NODE_EVENTS . PROPOSE_INSTALL , async ( msg ) => { const { data : { appInstanceId } } = msg ; confirmProposedAppInstance ( installationProposalRpc . parameters , await getAppInstanceProposal ( nodeA , appInstanceId ) ) ; nodeA . once ( NODE_EVENTS . INSTALL , async ( msg ) => { if ( msg . data . params . appInstanceId === appInstanceId ) { const appInstanceId = msg . data . params . appInstanceId ; const appInstanceNodeA = await getAppInstance ( nodeA , appInstanceId ) ; const appInstanceNodeB = await getAppInstance ( nodeB , appInstanceId ) ; expect ( appInstanceNodeA ) . toEqual ( appInstanceNodeB ) ; resolve ( [ appInstanceId , proposedParams ] ) ; } } ) ; await nodeB . rpcRouter . dispatch ( constructInstallRpc ( msg . data . appInstanceId ) ) ; } ) ; const response = await nodeA . rpcRouter . dispatch ( installationProposalRpc ) ; const { appInstanceId } = response . result . result as NodeTypes . ProposeInstallResult ; return appInstanceId ; } ) ; } export async function installVirtualApp ( nodeA , nodeB , nodeC , appDefinition , initialState ? , assetId ? , initiatorDeposit ? , responderDeposit ? ) { nodeC . on ( NODE_EVENTS . PROPOSE_INSTALL , async ( { data : { appInstanceId : eventAppInstanceId } } ) => { const { appInstanceId , params : { intermediaryIdentifier } } = await proposal ; if ( eventAppInstanceId === appInstanceId ) { nodeC . rpcRouter . dispatch ( constructInstallVirtualRpc ( appInstanceId , intermediaryIdentifier ) ) ; } } ) ; const proposal = makeVirtualProposal ( nodeA , nodeC , nodeB , appDefinition , initialState , assetId , initiatorDeposit , responderDeposit ) ; return new Promise ( ( resolve ) => nodeA . once ( NODE_EVENTS . INSTALL_VIRTUAL , async ( { data : { params : { appInstanceId : eventAppInstanceId } } } ) => { const { appInstanceId } = await proposal ; if ( eventAppInstanceId === appInstanceId ) resolve ( appInstanceId ) ; } ) ) ; } export async function confirmChannelCreation ( nodeA , nodeB , ownersPublicIdentifiers , data ) { const openChannelsNodeA = await getChannelAddresses ( nodeA ) ; const openChannelsNodeB = await getChannelAddresses ( nodeB ) ; expect ( openChannelsNodeA . has ( data . multisigAddress ) ) . toBeTruthy ( ) ; expect ( openChannelsNodeB . has ( data . multisigAddress ) ) . toBeTruthy ( ) ; expect ( data . owners ) . toEqual ( ownersPublicIdentifiers ) ; } export async function confirmAppInstanceInstallation ( proposedParams , appInstance ) { expect ( appInstance . appInterface . addr ) . toEqual ( proposedParams . appDefinition ) ; expect ( appInstance . appInterface . stateEncoding ) . toEqual ( proposedParams . abiEncodings . stateEncoding ) ; expect ( appInstance . appInterface . actionEncoding ) . toEqual ( proposedParams . abiEncodings . actionEncoding ) ; expect ( appInstance . defaultTimeout ) . toEqual ( proposedParams . timeout . toNumber ( ) ) ; expect ( appInstance . latestState ) . toEqual ( proposedParams . initialState ) ; } export async function getState ( nodeA , appInstanceId ) { const getStateReq = constructGetStateRpc ( appInstanceId ) ; const getStateResult = await nodeA . rpcRouter . dispatch ( getStateReq ) ; return ( getStateResult . result . result as NodeTypes . GetStateResult ) . state ; } export async function makeVirtualProposal ( nodeA , nodeC , nodeB , appDefinition , initialState ? , assetId ? , initiatorDeposit ? , responderDeposit ? ) { const appContext = getAppContext ( appDefinition , initialState ) ; const virtualProposalRpc = constructVirtualProposalRpc ( nodeC . publicIdentifier , nodeB . publicIdentifier , appContext . appDefinition , appContext . abiEncodings , appContext . initialState , initiatorDeposit || One , assetId || CONVENTION_FOR_ETH_TOKEN_ADDRESS , responderDeposit || Zero , assetId || CONVENTION_FOR_ETH_TOKEN_ADDRESS ) ; const params = virtualProposalRpc . parameters as NodeTypes . ProposeInstallVirtualParams ; const { result : { result : { appInstanceId } } } = await nodeA . rpcRouter . dispatch ( { parameters : params , methodName : NodeTypes . RpcMethodName . PROPOSE_INSTALL , id : Date . now ( ) } ) ; return { appInstanceId , params } ; } export async function installTTTVirtual ( node , appInstanceId , intermediaryIdentifier ) { return await node . rpcRouter . dispatch ( constructInstallVirtualRpc ( appInstanceId , intermediaryIdentifier ) ) ; } export async function makeInstallCall ( node , appInstanceId ) { return await node . rpcRouter . dispatch ( constructInstallRpc ( appInstanceId ) ) ; } export async function makeVirtualProposeCall ( nodeA , nodeC , nodeB , appDefinition , initialState ? ) { const appContext = getAppContext ( appDefinition , initialState ) ; const virtualProposalRpc = constructVirtualProposalRpc ( nodeC . publicIdentifier , nodeB . publicIdentifier , appContext . appDefinition , appContext . abiEncodings , appContext . initialState ) ; const response = await nodeA . rpcRouter . dispatch ( virtualProposalRpc ) ; return { appInstanceId : ( response . result as NodeTypes . ProposeInstallVirtualResult ) . appInstanceId , params : virtualProposalRpc . parameters as NodeTypes . ProposeInstallVirtualParams } ; } export function makeProposeCall ( nodeB , appDefinition , initialState ? , initiatorDeposit = Zero , initiatorDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , responderDeposit = Zero , responderDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const appContext = getAppContext ( appDefinition , initialState ) ; return constructAppProposalRpc ( nodeB . publicIdentifier , appContext . appDefinition , appContext . abiEncodings , appContext . initialState , initiatorDeposit , initiatorDepositTokenAddress , responderDeposit , responderDepositTokenAddress ) ; } export async function makeAndSendProposeCall ( nodeA , nodeB , appDefinition , initialState ? , initiatorDeposit = Zero , initiatorDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS , responderDeposit = Zero , responderDepositTokenAddress = CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { const installationProposalRpc = makeProposeCall ( nodeB , appDefinition , initialState , initiatorDeposit , initiatorDepositTokenAddress , responderDeposit , responderDepositTokenAddress ) ; const { result : { result : { appInstanceId } } } = await nodeA . rpcRouter . dispatch ( installationProposalRpc ) ; return { appInstanceId , params : installationProposalRpc . parameters as NodeTypes . ProposeInstallParams } ; } return export async function transferERC20Tokens ( toAddress , tokenAddress = global [ "networkContext" ] [ "DolphinCoin" ] , contractABI = DolphinCoin . abi , amount = One ) { const deployerAccount = new Wallet ( global [ "fundedPrivateKey" ] , new JsonRpcProvider ( global [ "ganacheURL" ] ) ) ; const contract = new Contract ( tokenAddress , contractABI , deployerAccount ) ; const balanceBefore = await contract . functions . balanceOf ( toAddress ) ; await contract . functions . transfer ( toAddress , amount ) ; const balanceAfter = await contract . functions . balanceOf ( toAddress ) ; expect ( balanceAfter . sub ( balanceBefore ) ) . toEqual ( amount ) ; return balanceAfter ; } export function getAppContext ( appDefinition , initialState ? , senderAddress ? , receiverAddress ? ) { const checkForAddresses = ( ) => { const missingAddr = ! senderAddress || ! receiverAddress ; if ( missingAddr && ! initialState ) { throw new Error ( "Must have sender and redeemer addresses to generate initial state for either transfer app context" ) ; } } ; switch ( appDefinition ) { case TicTacToeApp : return { appDefinition , abiEncodings : tttAbiEncodings , initialState : initialState || initialEmptyTTTState ( ) , outcomeType : OutcomeType . TWO_PARTY_FIXED_OUTCOME } ; case UnidirectionalTransferApp : checkForAddresses ( ) ; return { appDefinition , initialState : initialState || initialTransferState ( senderAddress ! , receiverAddress ! ) , abiEncodings : transferAbiEncodings , outcomeType : OutcomeType . SINGLE_ASSET_TWO_PARTY_COIN_TRANSFER } ; case UnidirectionalLinkedTransferApp : checkForAddresses ( ) ; const { state } = initialLinkedState ( senderAddress ! , receiverAddress ! ) ; return { appDefinition , initialState : initialState || state , abiEncodings : linkedAbiEncodings , outcomeType : OutcomeType . SINGLE_ASSET_TWO_PARTY_COIN_TRANSFER } ; case SimpleTransferApp : checkForAddresses ( ) ; return { appDefinition , initialState : initialState || initialSimpleTransferState ( senderAddress ! , receiverAddress ! ) , abiEncodings : simpleTransferAbiEncodings , outcomeType : OutcomeType . SINGLE_ASSET_TWO_PARTY_COIN_TRANSFER } ; default : throw new Error ( `Proposing the specified app is not supported: ${ appDefinition }` ) ; } } export async function uninstallVirtualApp ( node , counterparty , intermediaryPubId , appId ) { const rpc = constructUninstallVirtualRpc ( appId , intermediaryPubId ) ; return new Promise ( async ( resolve ) => { counterparty . once ( NODE_EVENTS . UNINSTALL_VIRTUAL , ( msg ) => { resolve ( msg . data . appInstanceId ) ; } ) ; await node . rpcRouter . dispatch ( rpc ) ; } ) ; } export async function takeAppAction ( node , appId , action ) { const res = await node . rpcRouter . dispatch ( constructTakeActionRpc ( appId , action ) ) ; return res . result . result ; } export async function uninstallApp ( node , counterparty , appId ) { return new Promise ( async ( resolve ) => { counterparty . once ( NODE_EVENTS . UNINSTALL , ( msg ) => { resolve ( msg . data . appInstanceId ) ; } ) ; await node . rpcRouter . dispatch ( constructUninstallRpc ( appId ) ) ; } ) ; } export async function getApps ( node ) { return ( await node . rpcRouter . dispatch ( constructGetAppsRpc ( ) ) ) . result . result . appInstances ; } export async function getBalances ( nodeA , nodeB , multisigAddress , tokenAddress ) { let tokenFreeBalanceState = await getFreeBalanceState ( nodeA , multisigAddress , tokenAddress ) ; const tokenBalanceNodeA = tokenFreeBalanceState [ xkeyKthAddress ( nodeA . publicIdentifier , 0 ) ] ; tokenFreeBalanceState = await getFreeBalanceState ( nodeB , multisigAddress , tokenAddress ) ; const tokenBalanceNodeB = tokenFreeBalanceState [ xkeyKthAddress ( nodeB . publicIdentifier , 0 ) ] ; return [ tokenBalanceNodeA , tokenBalanceNodeB ] ; }