import { OutcomeType } from "@counterfactual/types" ; import { Zero } from "ethers/constants" ; import { BigNumber , bigNumberify , getAddress } from "ethers/utils" ; import { CONVENTION_FOR_ETH_TOKEN_ADDRESS } from "../constants" ; import { xkeyKthAddress , xkeysToSortedKthAddresses } from "../engine/xkeys" ; import { getFreeBalanceAppInterface , merge } from "../ethereum/utils/free-balance-app" ; import { prettyPrintObject } from "../utils" ; import { AppInstance } from "./app-instance" ; const HARD_CODED_ASSUMPTIONS = { freeBalanceInitialStateTimeout : 172800 , appSequenceNumberForFreeBalance : 0 } ; export type CoinTransfer = { to : string ; amount : BigNumber ; } ; type CoinTransferJSON = { to : string ; amount : string ; } ; type FreeBalanceState = { activeAppsMap : ActiveAppsMap ; balancesIndexedByToken : { [ tokenAddress ] : CoinTransfer [ ] ; } ; } ; type FreeBalanceStateJSON = { tokenAddresses : string [ ] ; balances : CoinTransferJSON [ ] [ ] ; activeApps : string [ ] ; } ; export type CoinTransferMap = { [ to ] : BigNumber ; } ; export type TokenIndexedCoinTransferMap = { [ tokenAddress ] : CoinTransferMap ; } ; export type ActiveAppsMap = { [ appInstanceIdentityHash ] : true ; } ; export class FreeBalanceClass { private constructor ( private readonly activeAppsMap , private readonly balancesIndexedByToken ) { } public toFreeBalanceState ( ) { return { activeAppsMap : this . activeAppsMap , balancesIndexedByToken : this . balancesIndexedByToken } ; } public toTokenIndexedCoinTransferMap ( ) { const ret = { } ; for ( const tokenAddress of Object . keys ( this . balancesIndexedByToken ) ) { ret [ tokenAddress ] = convertCoinTransfersToCoinTransfersMap ( this . balancesIndexedByToken [ tokenAddress ] ) ; } return ret ; } public toAppInstance ( oldAppInstance ) { return oldAppInstance . setState ( serializeFreeBalanceState ( this . toFreeBalanceState ( ) ) ) ; } public static createWithFundedTokenAmounts ( addresses , amount , tokenAddresses ) { return new FreeBalanceClass ( { } , tokenAddresses . reduce ( ( balancesIndexedByToken , tokenAddress ) => ( { ... balancesIndexedByToken , [ tokenAddress ] : addresses . map ( to => ( { to , amount } ) ) } ) , { } as { [ tokenAddress ] : CoinTransfer [ ] ; } ) ) ; } public static fromAppInstance ( appInstance ) { const freeBalanceState = deserializeFreeBalanceState ( appInstance . state as FreeBalanceStateJSON ) ; return new FreeBalanceClass ( freeBalanceState . activeAppsMap , freeBalanceState . balancesIndexedByToken ) ; } public getBalance ( tokenAddress , beneficiary ) { try { return convertCoinTransfersToCoinTransfersMap ( this . balancesIndexedByToken [ tokenAddress ] ) [ beneficiary ] ; } catch { return Zero ; } } public withTokenAddress ( tokenAddress ) { let balances = { } ; balances = convertCoinTransfersToCoinTransfersMap ( this . balancesIndexedByToken [ tokenAddress ] ) ; if ( Object . keys ( balances ) . length === 0 ) { const addresses = Object . keys ( convertCoinTransfersToCoinTransfersMap ( this . balancesIndexedByToken [ CONVENTION_FOR_ETH_TOKEN_ADDRESS ] ) ) ; for ( const address of addresses ) { balances [ address ] = Zero ; } } return balances ; } public removeActiveApp ( activeApp ) { delete this . activeAppsMap [ activeApp ] ; return this ; } public addActiveApp ( activeApp ) { this . activeAppsMap [ activeApp ] = true ; return this ; } public prettyPrint ( ) { const balances = this . balancesIndexedByToken ; const ret = { } as any ; for ( const tokenAddress of Object . keys ( balances ) ) { const ret2 = { } as any ; for ( const coinTransfer of balances [ tokenAddress ] ) { ret2 [ coinTransfer . to ] = coinTransfer . amount ; } ret [ tokenAddress ] = ret2 ; } console . table ( ret ) ; } public increment ( increments ) { for ( const tokenAddress of Object . keys ( increments ) ) { const t1 = convertCoinTransfersToCoinTransfersMap ( this . balancesIndexedByToken [ tokenAddress ] ) ; const t2 = merge ( t1 , increments [ tokenAddress ] ) ; for ( const val of Object . values ( t2 ) ) { if ( val . lt ( Zero ) ) { throw Error ( `FreeBalanceClass::increment ended up with a negative balance when
            merging ${ prettyPrintObject ( t1 ) } and ${ prettyPrintObject ( increments [ tokenAddress ] ) }` ) ; } } this . balancesIndexedByToken [ tokenAddress ] = convertCoinTransfersMapToCoinTransfers ( t2 ) ; } return this ; } } /**
 * Note that the state of the Free Balance is held as plain types
 * and only converted to more complex types (i.e. BigNumber) upon usage.
 */ export function createFreeBalance ( userNeuteredExtendedKeys , coinBucketAddress , freeBalanceTimeout ) { const sortedTopLevelKeys = xkeysToSortedKthAddresses ( userNeuteredExtendedKeys , 0 ) ; const initialState = { activeAppsMap : { } , balancesIndexedByToken : { [ CONVENTION_FOR_ETH_TOKEN_ADDRESS ] : [ { to : sortedTopLevelKeys [ 0 ] , amount : Zero } , { to : sortedTopLevelKeys [ 1 ] , amount : Zero } ] } } ; return new AppInstance ( sortedTopLevelKeys , freeBalanceTimeout , getFreeBalanceAppInterface ( coinBucketAddress ) , false , HARD_CODED_ASSUMPTIONS . appSequenceNumberForFreeBalance , serializeFreeBalanceState ( initialState ) , 0 , HARD_CODED_ASSUMPTIONS . freeBalanceInitialStateTimeout , OutcomeType . MULTI_ASSET_MULTI_PARTY_COIN_TRANSFER ) ; } function deserializeFreeBalanceState ( freeBalanceStateJSON ) { const { activeApps , tokenAddresses , balances } = freeBalanceStateJSON ; return { balancesIndexedByToken : ( tokenAddresses || [ ] ) . reduce ( ( acc , tokenAddress , idx ) => ( { ... acc , [ getAddress ( tokenAddress ) ] : balances [ idx ] . map ( ( { to , amount } ) => ( { to , amount : bigNumberify ( amount ) } ) ) } ) , { } ) , activeAppsMap : ( activeApps || [ ] ) . reduce ( ( acc , identityHash ) => ( { ... acc , [ identityHash ] : true } ) , { } ) } ; } function serializeFreeBalanceState ( freeBalanceState ) { return { activeApps : Object . keys ( freeBalanceState . activeAppsMap ) , tokenAddresses : Object . keys ( freeBalanceState . balancesIndexedByToken ) , balances : Object . values ( freeBalanceState . balancesIndexedByToken ) . map ( balances => balances . map ( ( { to , amount } ) => ( { to , amount : amount . toHexString ( ) } ) ) ) } ; } export function convertCoinTransfersToCoinTransfersMap ( coinTransfers ) { return ( coinTransfers || [ ] ) . reduce ( ( acc , { to , amount } ) => ( { ... acc , [ to ] : amount } ) , { } ) ; } function convertCoinTransfersMapToCoinTransfers ( coinTransfersMap ) { return Object . entries ( coinTransfersMap ) . map ( ( [ to , amount ] ) => ( { to , amount } ) ) ; } /**
 * Address used for a Node's free balance
 */ export function getFreeBalanceAddress ( publicIdentifier ) { return xkeyKthAddress ( publicIdentifier , 0 ) ; }