import { coinBalanceRefundStateEncoding , NetworkContext , OutcomeType , singleAssetTwoPartyCoinTransferInterpreterParamsEncoding } from "@counterfactual/types" ; import { MaxUint256 } from "ethers/constants" ; import { BigNumber , defaultAbiCoder } from "ethers/utils" ; import { CONVENTION_FOR_ETH_TOKEN_ADDRESS , UNASSIGNED_SEQ_NO } from "../constants" ; import { ConditionalTransaction , SetStateCommitment , WithdrawERC20Commitment , WithdrawETHCommitment } from "../ethereum" ; import { AppInstance , StateChannel } from "../models" ; import { ProtocolExecutionFlow } from "." ; import { Opcode , Protocol } from "./enums" ; import { Context , ProtocolMessage , WithdrawParams } from "./types" ; import { assertIsValidSignature } from "./utils/signature-validator" ; const { IO_SEND , IO_SEND_AND_WAIT , OP_SIGN } = Opcode ; const { Install , Update , Withdraw } = Protocol ; description export const WITHDRAW_PROTOCOL = { param { Context } context 0 : async function * ( context ) { const { store , message : { params , processID } , network } = context ; const { sharedData : { stateChannelsMap } } = store ; const { responderXpub , multisigAddress , recipient , amount , tokenAddress } = params as WithdrawParams ; const preInstallRefundAppStateChannel = StateChannel . fromJson ( stateChannelsMap [ multisigAddress ] ) ; const responderAddress = preInstallRefundAppStateChannel . getFreeBalanceAddrOf ( responderXpub ) ; const postInstallRefundAppStateChannel = addRefundAppToStateChannel ( preInstallRefundAppStateChannel , params as WithdrawParams , network ) ; const refundApp = postInstallRefundAppStateChannel . mostRecentlyInstalledAppInstance ( ) ; const conditionalTransactionData = constructConditionalTransactionForRefundApp ( network , postInstallRefundAppStateChannel ) ; const mySignatureOnConditionalTransaction = yield [ OP_SIGN , conditionalTransactionData ] ; const { customData : { signature : counterpartySignatureOnConditionalTransaction , signature2 : counterpartySignatureOnFreeBalanceStateUpdate } } = yield [ IO_SEND_AND_WAIT , { processID , params , protocol : Withdraw , toXpub : responderXpub , customData : { signature : mySignatureOnConditionalTransaction } , seq : 1 } as ProtocolMessage ] ; assertIsValidSignature ( responderAddress , conditionalTransactionData , counterpartySignatureOnConditionalTransaction ) ; const signedConditionalTransaction = conditionalTransactionData . getSignedTransaction ( [ mySignatureOnConditionalTransaction , counterpartySignatureOnConditionalTransaction ] ) ; await store . setCommitment ( [ Install , refundApp . identityHash ] , signedConditionalTransaction ) ; const freeBalanceUpdateData = new SetStateCommitment ( network , postInstallRefundAppStateChannel . freeBalance . identity , postInstallRefundAppStateChannel . freeBalance . hashOfLatestState , postInstallRefundAppStateChannel . freeBalance . versionNumber , postInstallRefundAppStateChannel . freeBalance . timeout ) ; assertIsValidSignature ( responderAddress , freeBalanceUpdateData , counterpartySignatureOnFreeBalanceStateUpdate ) ; const mySignatureOnFreeBalanceStateUpdate = yield [ OP_SIGN , freeBalanceUpdateData ] ; const signedFreeBalanceStateUpdate = freeBalanceUpdateData . getSignedTransaction ( [ mySignatureOnFreeBalanceStateUpdate , counterpartySignatureOnFreeBalanceStateUpdate ] ) ; await store . setCommitment ( [ Update , postInstallRefundAppStateChannel . freeBalance . identityHash ] , signedFreeBalanceStateUpdate ) ; const withdrawCommitment = constructWithdrawalCommitment ( postInstallRefundAppStateChannel , recipient , amount , tokenAddress ) ; const mySignatureOnWithdrawalCommitment = yield [ OP_SIGN , withdrawCommitment ] ; const { customData : { signature : counterpartySignatureOnWithdrawalCommitment , signature2 : counterpartySignatureOnUninstallCommitment } } = yield [ IO_SEND_AND_WAIT , { processID , protocol : Withdraw , toXpub : responderXpub , customData : { signature : mySignatureOnFreeBalanceStateUpdate , signature2 : mySignatureOnWithdrawalCommitment } , seq : UNASSIGNED_SEQ_NO } as ProtocolMessage ] ; assertIsValidSignature ( responderAddress , withdrawCommitment , counterpartySignatureOnWithdrawalCommitment ) ; const postUninstallRefundAppStateChannel = postInstallRefundAppStateChannel . uninstallApp ( refundApp . identityHash , { } ) ; const uninstallRefundAppCommitment = new SetStateCommitment ( network , postUninstallRefundAppStateChannel . freeBalance . identity , postUninstallRefundAppStateChannel . freeBalance . hashOfLatestState , postUninstallRefundAppStateChannel . freeBalance . versionNumber , postUninstallRefundAppStateChannel . freeBalance . timeout ) ; assertIsValidSignature ( responderAddress , uninstallRefundAppCommitment , counterpartySignatureOnUninstallCommitment ) ; await store . saveStateChannel ( postUninstallRefundAppStateChannel ) ; const mySignatureOnUninstallCommitment = yield [ OP_SIGN , uninstallRefundAppCommitment ] ; yield < [ Opcode , ProtocolMessage ] > [ IO_SEND_AND_WAIT , { protocol : Withdraw , processID : context . message . processID , toXpub : responderXpub , customData : { signature : mySignatureOnUninstallCommitment } , seq : UNASSIGNED_SEQ_NO } ] ; const signedWithdrawalCommitment = withdrawCommitment . getSignedTransaction ( [ mySignatureOnWithdrawalCommitment , counterpartySignatureOnWithdrawalCommitment ] ) ; await store . storeWithdrawalCommitment ( multisigAddress , signedWithdrawalCommitment ) ; const signedUninstallCommitment = uninstallRefundAppCommitment . getSignedTransaction ( [ mySignatureOnUninstallCommitment , counterpartySignatureOnUninstallCommitment ] ) ; await store . setCommitment ( [ Update , postUninstallRefundAppStateChannel . freeBalance . identityHash ] , signedUninstallCommitment ) ; } , param { Context } context 1 : async function * ( context ) { const { store , message : { params , processID , customData } , network } = context ; const { sharedData : { stateChannelsMap } } = store ; const counterpartySignatureOnConditionalTransaction = customData . signature ; const { initiatorXpub , multisigAddress , recipient , amount , tokenAddress } = params as WithdrawParams ; const preInstallRefundAppStateChannel = StateChannel . fromJson ( stateChannelsMap [ multisigAddress ] ) ; const initiatorAddress = preInstallRefundAppStateChannel . getFreeBalanceAddrOf ( initiatorXpub ) ; const postInstallRefundAppStateChannel = addRefundAppToStateChannel ( preInstallRefundAppStateChannel , params as WithdrawParams , network ) ; const refundApp = postInstallRefundAppStateChannel . mostRecentlyInstalledAppInstance ( ) ; const conditionalTransactionData = constructConditionalTransactionForRefundApp ( network , postInstallRefundAppStateChannel ) ; assertIsValidSignature ( initiatorAddress , conditionalTransactionData , counterpartySignatureOnConditionalTransaction ) ; const mySignatureOnConditionalTransaction = yield [ OP_SIGN , conditionalTransactionData ] ; const signedConditionalTransaction = conditionalTransactionData . getSignedTransaction ( [ mySignatureOnConditionalTransaction , counterpartySignatureOnConditionalTransaction ] ) ; await store . setCommitment ( [ Install , refundApp . identityHash ] , signedConditionalTransaction ) ; const freeBalanceUpdateData = new SetStateCommitment ( network , postInstallRefundAppStateChannel . freeBalance . identity , postInstallRefundAppStateChannel . freeBalance . hashOfLatestState , postInstallRefundAppStateChannel . freeBalance . versionNumber , postInstallRefundAppStateChannel . freeBalance . timeout ) ; const mySignatureOnFreeBalanceStateUpdate = yield [ OP_SIGN , freeBalanceUpdateData ] ; const { customData : { signature : counterpartySignatureOnFreeBalanceStateUpdate , signature2 : counterpartySignatureOnWithdrawalCommitment } } = yield [ IO_SEND_AND_WAIT , { processID , protocol : Withdraw , toXpub : initiatorXpub , customData : { signature : mySignatureOnConditionalTransaction , signature2 : mySignatureOnFreeBalanceStateUpdate } , seq : UNASSIGNED_SEQ_NO } as ProtocolMessage ] ; assertIsValidSignature ( initiatorAddress , freeBalanceUpdateData , counterpartySignatureOnFreeBalanceStateUpdate ) ; const signedFreeBalanceStateUpdate = freeBalanceUpdateData . getSignedTransaction ( [ mySignatureOnFreeBalanceStateUpdate , counterpartySignatureOnFreeBalanceStateUpdate ] ) ; await store . setCommitment ( [ Update , postInstallRefundAppStateChannel . freeBalance . identityHash ] , signedFreeBalanceStateUpdate ) ; const withdrawCommitment = constructWithdrawalCommitment ( postInstallRefundAppStateChannel , recipient , amount , tokenAddress ) ; assertIsValidSignature ( initiatorAddress , withdrawCommitment , counterpartySignatureOnWithdrawalCommitment ) ; const mySignatureOnWithdrawalCommitment = yield [ OP_SIGN , withdrawCommitment ] ; const signedWithdrawalCommitment = withdrawCommitment . getSignedTransaction ( [ mySignatureOnWithdrawalCommitment , counterpartySignatureOnWithdrawalCommitment ] ) ; await store . storeWithdrawalCommitment ( multisigAddress , signedWithdrawalCommitment ) ; const postUninstallRefundAppStateChannel = postInstallRefundAppStateChannel . uninstallApp ( refundApp . identityHash , { } ) ; const uninstallRefundAppCommitment = new SetStateCommitment ( network , postUninstallRefundAppStateChannel . freeBalance . identity , postUninstallRefundAppStateChannel . freeBalance . hashOfLatestState , postUninstallRefundAppStateChannel . freeBalance . versionNumber , postUninstallRefundAppStateChannel . freeBalance . timeout ) ; const mySignatureOnUninstallCommitment = yield [ OP_SIGN , uninstallRefundAppCommitment ] ; const { customData : { signature : counterpartySignatureOnUninstallCommitment } } = yield [ IO_SEND_AND_WAIT , { processID , protocol : Withdraw , toXpub : initiatorXpub , customData : { signature : mySignatureOnWithdrawalCommitment , signature2 : mySignatureOnUninstallCommitment } , seq : UNASSIGNED_SEQ_NO } as ProtocolMessage ] ; assertIsValidSignature ( initiatorAddress , uninstallRefundAppCommitment , counterpartySignatureOnUninstallCommitment ) ; const signedUninstallCommitment = uninstallRefundAppCommitment . getSignedTransaction ( [ mySignatureOnUninstallCommitment , counterpartySignatureOnUninstallCommitment ] ) ; await store . saveStateChannel ( postUninstallRefundAppStateChannel ) ; await store . setCommitment ( [ Update , postUninstallRefundAppStateChannel . freeBalance . identityHash ] , signedUninstallCommitment ) ; yield [ IO_SEND , { processID , protocol : Withdraw , toXpub : initiatorXpub , customData : { dataPersisted : true } , seq : UNASSIGNED_SEQ_NO } as ProtocolMessage ] ; } } ; param { StateChannel } stateChannel param { WithdrawParams } params param { NetworkContext } network returns { StateChannel } function addRefundAppToStateChannel ( stateChannel , params , network ) { const { recipient , amount , multisigAddress , initiatorXpub , tokenAddress } = params ; const defaultTimeout = 1008 ; const refundAppInstance = new AppInstance ( stateChannel . getNextSigningKeys ( ) , defaultTimeout , { addr : network . CoinBalanceRefundApp , stateEncoding : coinBalanceRefundStateEncoding , actionEncoding : undefined } , false , stateChannel . numProposedApps , { recipient , multisig : multisigAddress , threshold : amount } , 0 , defaultTimeout , OutcomeType . SINGLE_ASSET_TWO_PARTY_COIN_TRANSFER , undefined , undefined , { tokenAddress , limit : MaxUint256 . toHexString ( ) } ) ; return stateChannel . installApp ( refundAppInstance , { [ tokenAddress ] : { [ stateChannel . getFreeBalanceAddrOf ( initiatorXpub ) ] : amount } } ) ; } param { NetworkContext } network param { StateChannel } stateChannel returns { ConditionalTransaction } function constructConditionalTransactionForRefundApp ( network , stateChannel ) { const appInstance = stateChannel . mostRecentlyInstalledAppInstance ( ) ; return new ConditionalTransaction ( network , stateChannel . multisigAddress , stateChannel . multisigOwners , appInstance . identityHash , stateChannel . freeBalance . identityHash , network . SingleAssetTwoPartyCoinTransferInterpreter , defaultAbiCoder . encode ( [ singleAssetTwoPartyCoinTransferInterpreterParamsEncoding ] , [ appInstance . singleAssetTwoPartyCoinTransferInterpreterParams ] ) ) ; } function constructWithdrawalCommitment ( postInstallRefundAppStateChannel , recipient , amount , tokenAddress ) { if ( tokenAddress === CONVENTION_FOR_ETH_TOKEN_ADDRESS ) { return new WithdrawETHCommitment ( postInstallRefundAppStateChannel . multisigAddress , postInstallRefundAppStateChannel . multisigOwners , recipient , amount ) ; } return new WithdrawERC20Commitment ( postInstallRefundAppStateChannel . multisigAddress , postInstallRefundAppStateChannel . multisigOwners , recipient , amount , tokenAddress ) ; }