import { AppInstanceJson , SolidityValueType } from "@counterfactual/types" ; import { BigNumber , bigNumberify } from "ethers/utils" ; import { xkeyKthAddress } from "../engine/xkeys" ; import { flip , flipTokenIndexedBalances } from "../ethereum/utils/free-balance-app" ; import { Store } from "../store" ; import { prettyPrintObject } from "../utils" ; import { AppInstanceProposal } from "." ; import { AppInstance } from "./app-instance" ; import { CoinTransferMap , createFreeBalance , FreeBalanceClass , TokenIndexedCoinTransferMap } from "./free-balance" ; export const HARD_CODED_ASSUMPTIONS = { freeBalanceDefaultTimeout : 172800 } ; const ERRORS = { APPS_NOT_EMPTY : ( size ) => `Expected the appInstances list to be empty but size ${ size }` , APP_DOES_NOT_EXIST : ( identityHash ) => `Attempted to edit an appInstance that does not exist: identity hash = ${ identityHash }` , FREE_BALANCE_MISSING : "Cannot find ETH Free Balance App in StateChannel" , FREE_BALANCE_IDX_CORRUPT : ( idx ) => `Index ${ idx } used to find ETH Free Balance is broken` , INSUFFICIENT_FUNDS : "Attempted to install an appInstance without sufficient funds" , MULTISIG_OWNERS_NOT_SORTED : "multisigOwners parameter of StateChannel must be sorted" } ; function sortAddresses ( addrs ) { return addrs . sort ( ( a , b ) => ( parseInt ( a , 16 ) < parseInt ( b , 16 ) ? - 1 : 1 ) ) ; } export type SingleAssetTwoPartyIntermediaryAgreement = { timeLockedPassThroughIdentityHash : string ; capitalProvided : string ; capitalProvider : string ; virtualAppUser : string ; tokenAddress : string ; } ; export type StateChannelJSON = { readonly multisigAddress : string ; readonly userNeuteredExtendedKeys : string [ ] ; readonly proposedAppInstances : [ string , AppInstanceProposal ] [ ] ; readonly appInstances : [ string , AppInstanceJson ] [ ] ; readonly singleAssetTwoPartyIntermediaryAgreements : [ string , SingleAssetTwoPartyIntermediaryAgreement ] [ ] ; readonly freeBalanceAppInstance : AppInstanceJson | undefined ; readonly monotonicNumProposedApps : number ; } ; export class StateChannel { constructor ( public readonly multisigAddress , public readonly userNeuteredExtendedKeys , readonly proposedAppInstances = new Map < string , AppInstanceProposal > ( [ ] ) , readonly appInstances = new Map < string , AppInstance > ( [ ] ) , readonly singleAssetTwoPartyIntermediaryAgreements = new Map < string , SingleAssetTwoPartyIntermediaryAgreement > ( [ ] ) , private readonly freeBalanceAppInstance ? , private readonly monotonicNumProposedApps = 0 ) { userNeuteredExtendedKeys . forEach ( xpub => { if ( ! xpub . startsWith ( "xpub" ) ) { throw Error ( `StateChannel constructor given invalid extended keys: ${ prettyPrintObject ( userNeuteredExtendedKeys ) }` ) ; } } ) ; } public get multisigOwners ( ) { return this . getSigningKeysFor ( 0 ) ; } public get numProposedApps ( ) { return this . monotonicNumProposedApps ; } public get numActiveApps ( ) { return this . appInstances . size ; } public getAppInstance ( appInstanceIdentityHash ) { if ( ! this . appInstances . has ( appInstanceIdentityHash ) ) { throw Error ( ERRORS . APP_DOES_NOT_EXIST ( appInstanceIdentityHash ) ) ; } return this . appInstances . get ( appInstanceIdentityHash ) ! ; } public hasAppInstance ( appInstanceId ) { return this . appInstances . has ( appInstanceId ) ; } public hasAppInstanceOfKind ( address ) { return ( Array . from ( this . appInstances . values ( ) ) . filter ( ( appInstance ) => { return appInstance . appInterface . addr === address ; } ) . length > 0 ) ; } public mostRecentlyInstalledAppInstance ( ) { if ( this . appInstances . size === 0 ) { throw Error ( "There are no installed AppInstances in this StateChannel" ) ; } return [ ... this . appInstances . values ( ) ] . reduce ( ( prev , current ) => current . appSeqNo > prev . appSeqNo ? current : prev ) ; } public mostRecentlyProposedAppInstance ( ) { if ( this . proposedAppInstances . size === 0 ) { throw Error ( "There are no proposed AppInstances in this StateChannel" ) ; } return [ ... this . proposedAppInstances . values ( ) ] . reduce ( ( prev , current ) => current . appSeqNo > prev . appSeqNo ? current : prev ) ; } public getAppInstanceOfKind ( address ) { const appInstances = Array . from ( this . appInstances . values ( ) ) . filter ( ( appInstance ) => { return appInstance . appInterface . addr === address ; } ) ; if ( appInstances . length !== 1 ) { throw Error ( `No AppInstance of addr ${ address } exists on channel: ${ this . multisigAddress }` ) ; } return appInstances [ 0 ] ; } public isAppInstanceInstalled ( appInstanceIdentityHash ) { return this . appInstances . has ( appInstanceIdentityHash ) ; } public getSigningKeysFor ( addressIndex ) { return sortAddresses ( this . userNeuteredExtendedKeys . map ( xpub => xkeyKthAddress ( xpub , addressIndex ) ) ) ; } public getNextSigningKeys ( ) { return this . getSigningKeysFor ( this . monotonicNumProposedApps ) ; } public get hasFreeBalance ( ) : boolean { return ! ! this . freeBalanceAppInstance ; } public get freeBalance ( ) : AppInstance { if ( this . freeBalanceAppInstance ) { return this . freeBalanceAppInstance ; } throw Error ( "There is no free balance app instance installed in this state channel" ) ; } public getMultisigOwnerAddrOf ( xpub ) { const [ alice , bob ] = this . multisigOwners ; const topLevelKey = xkeyKthAddress ( xpub , 0 ) ; if ( topLevelKey !== alice && topLevelKey !== bob ) { throw Error ( `getMultisigOwnerAddrOf received invalid xpub not in multisigOwners: ${ xpub }` ) ; } return topLevelKey ; } public getFreeBalanceAddrOf ( xpub ) { const [ alice , bob ] = this . freeBalanceAppInstance ! . participants ; const topLevelKey = xkeyKthAddress ( xpub , 0 ) ; if ( topLevelKey !== alice && topLevelKey !== bob ) { throw Error ( `getFreeBalanceAddrOf received invalid xpub without free balance account: ${ xpub }` ) ; } return topLevelKey ; } public getFreeBalanceClass ( ) { return FreeBalanceClass . fromAppInstance ( this . freeBalance ) ; } private build ( args ) { return new StateChannel ( args . multisigAddress || this . multisigAddress , args . userNeuteredExtendedKeys || this . userNeuteredExtendedKeys , args . proposedAppInstances || this . proposedAppInstances , args . appInstances || this . appInstances , args . singleAssetTwoPartyIntermediaryAgreements || this . singleAssetTwoPartyIntermediaryAgreements , args . freeBalanceAppInstance || this . freeBalanceAppInstance , args . monotonicNumProposedApps || this . monotonicNumProposedApps ) ; } public incrementFreeBalance ( increments ) { return this . build ( { freeBalanceAppInstance : this . getFreeBalanceClass ( ) . increment ( increments ) . toAppInstance ( this . freeBalance ) } ) ; } public addActiveApp ( activeApp ) { return this . build ( { freeBalanceAppInstance : this . getFreeBalanceClass ( ) . addActiveApp ( activeApp ) . toAppInstance ( this . freeBalance ) } ) ; } public removeActiveApp ( activeApp ) { return this . build ( { freeBalanceAppInstance : this . getFreeBalanceClass ( ) . removeActiveApp ( activeApp ) . toAppInstance ( this . freeBalance ) } ) ; } public addActiveAppAndIncrementFreeBalance ( activeApp , tokenIndexedIncrements ) { return this . incrementFreeBalance ( tokenIndexedIncrements ) . addActiveApp ( activeApp ) ; } public removeActiveAppAndIncrementFreeBalance ( activeApp , tokenIndexedIncrements ) { return this . removeActiveApp ( activeApp ) . incrementFreeBalance ( tokenIndexedIncrements ) ; } public setFreeBalance ( newFreeBalanceClass ) { return this . build ( { freeBalanceAppInstance : newFreeBalanceClass . toAppInstance ( this . freeBalance ) } ) ; } public static setupChannel ( freeBalanceAppAddress , multisigAddress , userNeuteredExtendedKeys , freeBalanceTimeout ? ) { return new StateChannel ( multisigAddress , userNeuteredExtendedKeys , new Map < string , AppInstanceProposal > ( [ ] ) , new Map < string , AppInstance > ( [ ] ) , new Map < string , SingleAssetTwoPartyIntermediaryAgreement > ( ) , createFreeBalance ( userNeuteredExtendedKeys , freeBalanceAppAddress , freeBalanceTimeout || HARD_CODED_ASSUMPTIONS . freeBalanceDefaultTimeout ) , 1 ) ; } public static createEmptyChannel ( multisigAddress , userNeuteredExtendedKeys ) { return new StateChannel ( multisigAddress , userNeuteredExtendedKeys , new Map < string , AppInstanceProposal > ( [ ] ) , new Map < string , AppInstance > ( ) , new Map < string , SingleAssetTwoPartyIntermediaryAgreement > ( ) , undefined , 1 ) ; } public addProposal ( proposal ) { const proposedAppInstances = new Map < string , AppInstanceProposal > ( this . proposedAppInstances . entries ( ) ) ; proposedAppInstances . set ( proposal . identityHash , proposal ) ; return this . build ( { proposedAppInstances , monotonicNumProposedApps : this . monotonicNumProposedApps + 1 } ) ; } public removeProposal ( appInstanceId ) { const proposedAppInstances = new Map < string , AppInstanceProposal > ( this . proposedAppInstances . entries ( ) ) ; proposedAppInstances . delete ( appInstanceId ) ; return this . build ( { proposedAppInstances } ) ; } public addAppInstance ( appInstance ) { const appInstances = new Map < string , AppInstance > ( this . appInstances . entries ( ) ) ; appInstances . set ( appInstance . identityHash , appInstance ) ; return this . build ( { appInstances , monotonicNumProposedApps : this . monotonicNumProposedApps + 1 } ) ; } public removeAppInstance ( appInstanceId ) { const appInstances = new Map < string , AppInstance > ( this . appInstances . entries ( ) ) ; appInstances . delete ( appInstanceId ) ; return this . build ( { appInstances } ) ; } public setState ( appInstanceIdentityHash , state ) { const appInstance = this . getAppInstance ( appInstanceIdentityHash ) ; const appInstances = new Map < string , AppInstance > ( this . appInstances . entries ( ) ) ; appInstances . set ( appInstanceIdentityHash , appInstance . setState ( state ) ) ; return this . build ( { appInstances } ) ; } public addSingleAssetTwoPartyIntermediaryAgreement ( targetIdentityHash , agreement , decrements , tokenAddress ) { const evaaInstances = new Map < string , SingleAssetTwoPartyIntermediaryAgreement > ( this . singleAssetTwoPartyIntermediaryAgreements . entries ( ) ) ; evaaInstances . set ( targetIdentityHash , agreement ) ; return this . build ( { singleAssetTwoPartyIntermediaryAgreements : evaaInstances } ) . addActiveAppAndIncrementFreeBalance ( targetIdentityHash , { [ tokenAddress ] : flip ( decrements ) } ) ; } public removeSingleAssetTwoPartyIntermediaryAgreement ( targetIdentityHash , increments , tokenAddress ) { const singleAssetTwoPartyIntermediaryAgreements = new Map < string , SingleAssetTwoPartyIntermediaryAgreement > ( this . singleAssetTwoPartyIntermediaryAgreements . entries ( ) ) ; if ( ! singleAssetTwoPartyIntermediaryAgreements . delete ( targetIdentityHash ) ) { throw Error ( `cannot find agreement with target hash ${ targetIdentityHash }` ) ; } return this . build ( { singleAssetTwoPartyIntermediaryAgreements } ) . removeActiveAppAndIncrementFreeBalance ( targetIdentityHash , { [ tokenAddress ] : increments } ) ; } public installApp ( appInstance , tokenIndexedDecrements ) { const participants = this . getSigningKeysFor ( appInstance . appSeqNo ) ; if ( ! participants . every ( ( v , idx ) => v === appInstance . participants [ idx ] ) ) { throw Error ( "AppInstance passed to installApp has incorrect participants" ) ; } const appInstances = new Map < string , AppInstance > ( this . appInstances . entries ( ) ) ; appInstances . set ( appInstance . identityHash , appInstance ) ; return this . build ( { appInstances } ) . addActiveAppAndIncrementFreeBalance ( appInstance . identityHash , flipTokenIndexedBalances ( tokenIndexedDecrements ) ) ; } public uninstallApp ( appInstanceIdentityHash , tokenIndexedIncrements ) { const appToBeUninstalled = this . getAppInstance ( appInstanceIdentityHash ) ; if ( appToBeUninstalled . identityHash !== appInstanceIdentityHash ) { throw Error ( `Consistency error: app stored under key ${ appInstanceIdentityHash } has identityHah ${ appToBeUninstalled . identityHash }` ) ; } const appInstances = new Map < string , AppInstance > ( this . appInstances . entries ( ) ) ; if ( ! appInstances . delete ( appToBeUninstalled . identityHash ) ) { throw Error ( `Consistency error: managed to call get on ${ appInstanceIdentityHash } but failed to call delete` ) ; } return this . build ( { appInstances } ) . removeActiveAppAndIncrementFreeBalance ( appInstanceIdentityHash , tokenIndexedIncrements ) ; } public getSingleAssetTwoPartyIntermediaryAgreementFromVirtualApp ( key ) { const ret = this . singleAssetTwoPartyIntermediaryAgreements . get ( key ) ; if ( ! ret ) { throw Error ( `Could not find any eth virtual app agreements with virtual app ${ key }` ) ; } return ret ; } toJson ( ) { return { multisigAddress : this . multisigAddress , userNeuteredExtendedKeys : this . userNeuteredExtendedKeys , proposedAppInstances : [ ... this . proposedAppInstances . entries ( ) ] , appInstances : [ ... this . appInstances . entries ( ) ] . map ( ( appInstanceEntry ) : [ string , AppInstanceJson ] => { return [ appInstanceEntry [ 0 ] , appInstanceEntry [ 1 ] . toJson ( ) ] ; } ) , freeBalanceAppInstance : ! ! this . freeBalanceAppInstance ? this . freeBalanceAppInstance . toJson ( ) : undefined , monotonicNumProposedApps : this . monotonicNumProposedApps , singleAssetTwoPartyIntermediaryAgreements : [ ... this . singleAssetTwoPartyIntermediaryAgreements . entries ( ) ] } ; } static fromJson ( json ) { return new StateChannel ( json . multisigAddress , json . userNeuteredExtendedKeys , new Map ( [ ... Object . values ( json . proposedAppInstances || [ ] ) ] . map ( ( proposal ) : [ string , AppInstanceProposal ] => { return [ proposal [ 0 ] , proposal [ 1 ] ] ; } ) ) , new Map ( [ ... Object . values ( json . appInstances || [ ] ) ] . map ( ( appInstanceEntry ) : [ string , AppInstance ] => { return [ appInstanceEntry [ 0 ] , AppInstance . fromJson ( appInstanceEntry [ 1 ] ) ] ; } ) ) , new Map ( json . singleAssetTwoPartyIntermediaryAgreements || [ ] ) , json . freeBalanceAppInstance ? AppInstance . fromJson ( json . freeBalanceAppInstance ) : undefined , json . monotonicNumProposedApps ) ; } static async getPeersAddressFromChannel ( myIdentifier , store , multisigAddress ) { const stateChannel = await store . getStateChannel ( multisigAddress ) ; const owners = stateChannel . userNeuteredExtendedKeys ; return owners . filter ( owner => owner !== myIdentifier ) ; } static async getPeersAddressFromAppInstanceID ( myIdentifier , store , appInstanceId ) { const multisigAddress = await store . getMultisigAddressFromAppInstance ( appInstanceId ) ; return StateChannel . getPeersAddressFromChannel ( myIdentifier , store , multisigAddress ) ; } }