diff --git a/sdk/state/close.go b/sdk/state/close.go index 60b8c8f3..46c02145 100644 --- a/sdk/state/close.go +++ b/sdk/state/close.go @@ -93,7 +93,11 @@ func (c *Channel) ProposeClose() (CloseAgreement, error) { } // If the channel is not open yet, error. - if c.latestAuthorizedCloseAgreement.isEmpty() { + cs, err := c.State() + if err != nil { + return CloseAgreement{}, fmt.Errorf("getting channel state: %w", err) + } + if cs < StateOpen { return CloseAgreement{}, fmt.Errorf("cannot propose a coordinated close before channel is opened") } @@ -122,9 +126,14 @@ func (c *Channel) ProposeClose() (CloseAgreement, error) { func (c *Channel) validateClose(ca CloseAgreement) error { // If the channel is not open yet, error. - if c.latestAuthorizedCloseAgreement.isEmpty() { + cs, err := c.State() + if err != nil { + return fmt.Errorf("getting channel state: %w", err) + } + if cs < StateOpen { return fmt.Errorf("cannot confirm a coordinated close before channel is opened") } + if ca.Details.IterationNumber != c.latestAuthorizedCloseAgreement.Details.IterationNumber { return fmt.Errorf("close agreement iteration number does not match saved latest authorized close agreement") } diff --git a/sdk/state/close_test.go b/sdk/state/close_test.go index 6c7f86b6..f5a7a4b3 100644 --- a/sdk/state/close_test.go +++ b/sdk/state/close_test.go @@ -4,8 +4,10 @@ import ( "testing" "time" + "github.com/stellar/experimental-payment-channels/sdk/txbuildtest" "github.com/stellar/go/keypair" "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -107,16 +109,41 @@ func TestChannel_ProposeClose(t *testing.T) { MaxOpenExpiry: 2 * time.Hour, }) - open1, err := localChannel.ProposeOpen(OpenParams{ - ObservationPeriodTime: 1, - ObservationPeriodLedgerGap: 1, - ExpiresAt: time.Now().Add(time.Hour), - }) - require.NoError(t, err) - open2, err := remoteChannel.ConfirmOpen(open1) - require.NoError(t, err) - _, err = localChannel.ConfirmOpen(open2) - require.NoError(t, err) + // Put channel into the Open state. + { + open1, err := localChannel.ProposeOpen(OpenParams{ + ObservationPeriodTime: 1, + ObservationPeriodLedgerGap: 1, + ExpiresAt: time.Now().Add(time.Hour), + }) + require.NoError(t, err) + open2, err := remoteChannel.ConfirmOpen(open1) + require.NoError(t, err) + _, err = localChannel.ConfirmOpen(open2) + require.NoError(t, err) + + ftx, err := localChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = localChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = remoteChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } // If the local proposes a close, the agreement will have them as the proposer. closeByLocal, err := localChannel.ProposeClose() diff --git a/sdk/state/ingest_test.go b/sdk/state/ingest_test.go index a0325fa5..f0bcec01 100644 --- a/sdk/state/ingest_test.go +++ b/sdk/state/ingest_test.go @@ -65,6 +65,8 @@ func TestChannel_IngestTx_latestUnauthorizedDeclTxViaFeeBump(t *testing.T) { // Mock initiatorChannel ingested formation tx successfully. initiatorChannel.openExecutedAndValidated = true + initiatorChannel.setInitiatorEscrowAccountSequence(initiatorEscrowAccount.SequenceNumber + 1) + responderChannel.openExecutedAndValidated = true // To prevent xdr parsing error. placeholderXDR := "AAAAAgAAAAIAAAADABArWwAAAAAAAAAAWPnYf+6kQN3t44vgesQdWh4JOOPj7aer852I7RJhtzAAAAAWg8TZOwANrPwAAAAKAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABABArWwAAAAAAAAAAWPnYf+6kQN3t44vgesQdWh4JOOPj7aer852I7RJhtzAAAAAWg8TZOwANrPwAAAALAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAD/39AAAAAAAAAAD49aUpVx7fhJPK6wDdlPJgkA1HkAi85qUL1tii8YSZzQAAABdjSVwcAA/8sgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAECtbAAAAAAAAAAD49aUpVx7fhJPK6wDdlPJgkA1HkAi85qUL1tii8YSZzQAAABee5CYcAA/8sgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAECtbAAAAAAAAAABY+dh/7qRA3e3ji+B6xB1aHgk44+Ptp6vznYjtEmG3MAAAABaDxNk7AA2s/AAAAAsAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAECtbAAAAAAAAAABY+dh/7qRA3e3ji+B6xB1aHgk44+Ptp6vznYjtEmG3MAAAABZIKg87AA2s/AAAAAsAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=" @@ -153,6 +155,8 @@ func TestChannel_IngestTx_latestUnauthorizedDeclTx(t *testing.T) { // Mock initiatorChannel ingested formation tx successfully. initiatorChannel.openExecutedAndValidated = true + initiatorChannel.setInitiatorEscrowAccountSequence(initiatorEscrowAccount.SequenceNumber + 1) + responderChannel.openExecutedAndValidated = true // To prevent xdr parsing error. placeholderXDR := "AAAAAgAAAAIAAAADABArWwAAAAAAAAAAWPnYf+6kQN3t44vgesQdWh4JOOPj7aer852I7RJhtzAAAAAWg8TZOwANrPwAAAAKAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABABArWwAAAAAAAAAAWPnYf+6kQN3t44vgesQdWh4JOOPj7aer852I7RJhtzAAAAAWg8TZOwANrPwAAAALAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAD/39AAAAAAAAAAD49aUpVx7fhJPK6wDdlPJgkA1HkAi85qUL1tii8YSZzQAAABdjSVwcAA/8sgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAECtbAAAAAAAAAAD49aUpVx7fhJPK6wDdlPJgkA1HkAi85qUL1tii8YSZzQAAABee5CYcAA/8sgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAECtbAAAAAAAAAABY+dh/7qRA3e3ji+B6xB1aHgk44+Ptp6vznYjtEmG3MAAAABaDxNk7AA2s/AAAAAsAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAECtbAAAAAAAAAABY+dh/7qRA3e3ji+B6xB1aHgk44+Ptp6vznYjtEmG3MAAAABZIKg87AA2s/AAAAAsAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=" @@ -294,6 +298,8 @@ func TestChannel_IngestTx_oldDeclTx(t *testing.T) { // Mock initiatorChannel ingested formation tx successfully. initiatorChannel.openExecutedAndValidated = true + initiatorChannel.setInitiatorEscrowAccountSequence(initiatorEscrowAccount.SequenceNumber + 1) + responderChannel.openExecutedAndValidated = true // To prevent xdr parsing error. placeholderXDR := "AAAAAgAAAAIAAAADABArWwAAAAAAAAAAWPnYf+6kQN3t44vgesQdWh4JOOPj7aer852I7RJhtzAAAAAWg8TZOwANrPwAAAAKAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABABArWwAAAAAAAAAAWPnYf+6kQN3t44vgesQdWh4JOOPj7aer852I7RJhtzAAAAAWg8TZOwANrPwAAAALAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAD/39AAAAAAAAAAD49aUpVx7fhJPK6wDdlPJgkA1HkAi85qUL1tii8YSZzQAAABdjSVwcAA/8sgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAECtbAAAAAAAAAAD49aUpVx7fhJPK6wDdlPJgkA1HkAi85qUL1tii8YSZzQAAABee5CYcAA/8sgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAECtbAAAAAAAAAABY+dh/7qRA3e3ji+B6xB1aHgk44+Ptp6vznYjtEmG3MAAAABaDxNk7AA2s/AAAAAsAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAECtbAAAAAAAAAABY+dh/7qRA3e3ji+B6xB1aHgk44+Ptp6vznYjtEmG3MAAAABZIKg87AA2s/AAAAAsAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=" diff --git a/sdk/state/integrationtests/state_test.go b/sdk/state/integrationtests/state_test.go index 74b01356..2a3f1fc7 100644 --- a/sdk/state/integrationtests/state_test.go +++ b/sdk/state/integrationtests/state_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stellar/experimental-payment-channels/sdk/state" + "github.com/stellar/experimental-payment-channels/sdk/txbuildtest" "github.com/stellar/go/amount" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/keypair" @@ -108,6 +109,31 @@ func TestOpenUpdatesUncoordinatedClose(t *testing.T) { require.NoError(t, err) } + { + t.Log("Initiator and Responder channels ingest the formation tx ...") + ftx, err := initiatorChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: initiator.KP.Address(), + ResponderSigner: responder.KP.Address(), + InitiatorEscrow: initiator.Escrow.Address(), + ResponderEscrow: responder.Escrow.Address(), + StartSequence: s, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = initiatorChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = responderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + t.Log("Iteration", i, "Declarations:", txSeqs(declarationTxs)) t.Log("Iteration", i, "Closes:", txSeqs(closeTxs)) @@ -343,6 +369,31 @@ func TestOpenUpdatesCoordinatedCloseStartCloseThenCoordinate(t *testing.T) { require.NoError(t, err) } + { + t.Log("Initiator and Responder channels ingest the formation tx ...") + ftx, err := initiatorChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: initiator.KP.Address(), + ResponderSigner: responder.KP.Address(), + InitiatorEscrow: initiator.Escrow.Address(), + ResponderEscrow: responder.Escrow.Address(), + StartSequence: s, + Asset: txnbuild.CreditAsset{Code: asset.Code(), Issuer: asset.Issuer()}, + }) + require.NoError(t, err) + + err = initiatorChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = responderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // Update balances known for each other. initiatorChannel.UpdateRemoteEscrowAccountBalance(responder.Contribution) responderChannel.UpdateRemoteEscrowAccountBalance(initiator.Contribution) @@ -524,6 +575,31 @@ func TestOpenUpdatesCoordinatedCloseCoordinateThenStartClose(t *testing.T) { require.NoError(t, err) } + { + t.Log("Initiator and Responder channels ingest the formation tx ...") + ftx, err := initiatorChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: initiator.KP.Address(), + ResponderSigner: responder.KP.Address(), + InitiatorEscrow: initiator.Escrow.Address(), + ResponderEscrow: responder.Escrow.Address(), + StartSequence: s, + Asset: txnbuild.CreditAsset{Code: asset.Code(), Issuer: asset.Issuer()}, + }) + require.NoError(t, err) + + err = initiatorChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = responderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // Update balances known for each other. initiatorChannel.UpdateRemoteEscrowAccountBalance(responder.Contribution) responderChannel.UpdateRemoteEscrowAccountBalance(initiator.Contribution) @@ -705,6 +781,31 @@ func TestOpenUpdatesCoordinatedCloseCoordinateThenStartCloseByRemote(t *testing. require.NoError(t, err) } + { + t.Log("Initiator and Responder channels ingest the formation tx ...") + ftx, err := initiatorChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: initiator.KP.Address(), + ResponderSigner: responder.KP.Address(), + InitiatorEscrow: initiator.Escrow.Address(), + ResponderEscrow: responder.Escrow.Address(), + StartSequence: s, + Asset: txnbuild.CreditAsset{Code: asset.Code(), Issuer: asset.Issuer()}, + }) + require.NoError(t, err) + + err = initiatorChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = responderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // Update balances known for each other. initiatorChannel.UpdateRemoteEscrowAccountBalance(responder.Contribution) responderChannel.UpdateRemoteEscrowAccountBalance(initiator.Contribution) @@ -865,6 +966,31 @@ func TestOpenUpdatesUncoordinatedClose_recieverNotReturningSigs(t *testing.T) { require.NoError(t, err) } + { + t.Log("Initiator and Responder channels ingest the formation tx ...") + ftx, err := initiatorChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: initiator.KP.Address(), + ResponderSigner: responder.KP.Address(), + InitiatorEscrow: initiator.Escrow.Address(), + ResponderEscrow: responder.Escrow.Address(), + StartSequence: s, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = initiatorChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = responderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // Update balances known for each other. initiatorChannel.UpdateRemoteEscrowAccountBalance(responder.Contribution) responderChannel.UpdateRemoteEscrowAccountBalance(initiator.Contribution) diff --git a/sdk/state/open.go b/sdk/state/open.go index 767059ec..5a769173 100644 --- a/sdk/state/open.go +++ b/sdk/state/open.go @@ -192,9 +192,14 @@ func (c *Channel) OpenTx() (formationTx *txnbuild.Transaction, err error) { // initiating the channel. func (c *Channel) ProposeOpen(p OpenParams) (OpenAgreement, error) { // if the channel is already open, error. - if c.openAgreement.isFull() { - return OpenAgreement{}, fmt.Errorf("cannot propose a new open if channel is already opened") + cs, err := c.State() + if err != nil { + return OpenAgreement{}, fmt.Errorf("getting channel state: %w", err) + } + if cs >= StateOpen { + return OpenAgreement{}, fmt.Errorf("cannot propose a new open if channel has already opened") } + c.startingSequence = c.initiatorEscrowAccount().SequenceNumber + 1 d := OpenAgreementDetails{ @@ -224,7 +229,11 @@ func (c *Channel) ProposeOpen(p OpenParams) (OpenAgreement, error) { func (c *Channel) validateOpen(m OpenAgreement) error { // if the channel is already open, error. - if c.openAgreement.isFull() { + cs, err := c.State() + if err != nil { + return fmt.Errorf("getting channel state: %w", err) + } + if cs >= StateOpen { return fmt.Errorf("cannot confirm a new open if channel is already opened") } diff --git a/sdk/state/open_test.go b/sdk/state/open_test.go index 3ff9f2fe..701bb3c3 100644 --- a/sdk/state/open_test.go +++ b/sdk/state/open_test.go @@ -5,8 +5,10 @@ import ( "testing" "time" + "github.com/stellar/experimental-payment-channels/sdk/txbuildtest" "github.com/stellar/go/keypair" "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -384,46 +386,69 @@ func TestChannel_ProposeAndConfirmOpen_rejectIfChannelAlreadyOpen(t *testing.T) SequenceNumber: int64(202), } - channel := NewChannel(Config{ + initiatorChannel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: true, LocalSigner: localSigner, RemoteSigner: remoteSigner.FromAddress(), LocalEscrowAccount: localEscrowAccount, RemoteEscrowAccount: remoteEscrowAccount, + MaxOpenExpiry: 2 * time.Hour, + }) + responderChannel := NewChannel(Config{ + NetworkPassphrase: network.TestNetworkPassphrase, + Initiator: false, + LocalSigner: remoteSigner, + RemoteSigner: localSigner.FromAddress(), + LocalEscrowAccount: remoteEscrowAccount, + RemoteEscrowAccount: localEscrowAccount, + MaxOpenExpiry: 2 * time.Hour, }) - channel.openAgreement = OpenAgreement{ - Details: OpenAgreementDetails{ - ObservationPeriodTime: 1, - ObservationPeriodLedgerGap: 1, - Asset: NativeAsset, - ExpiresAt: time.Now(), - ProposingSigner: localSigner.FromAddress(), - ConfirmingSigner: remoteSigner.FromAddress(), - }, - ProposerSignatures: OpenAgreementSignatures{ - Declaration: xdr.Signature{0}, - Close: xdr.Signature{1}, - Formation: xdr.Signature{2}, - }, - ConfirmerSignatures: OpenAgreementSignatures{ - Declaration: xdr.Signature{3}, - Close: xdr.Signature{4}, - Formation: xdr.Signature{5}, - }, - } - _, err := channel.ProposeOpen(OpenParams{}) - require.EqualError(t, err, "cannot propose a new open if channel is already opened") + // Open channel. + m, err := initiatorChannel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Second), + ObservationPeriodTime: 10, + ObservationPeriodLedgerGap: 10, + }) + require.NoError(t, err) + m, err = responderChannel.ConfirmOpen(m) + require.NoError(t, err) + _, err = initiatorChannel.ConfirmOpen(m) + require.NoError(t, err) - _, err = channel.ConfirmOpen(OpenAgreement{}) - require.EqualError(t, err, "validating open agreement: cannot confirm a new open if channel is already opened") + // Ingest the formationTx successfully to enter the Open state. + ftx, err := initiatorChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) - // A channel without a full open agreement should be able to propose an open - channel.openAgreement.ConfirmerSignatures = OpenAgreementSignatures{} - _, err = channel.ProposeOpen(OpenParams{ - Asset: NativeAsset, - ExpiresAt: time.Now().Add(5 * time.Minute), + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, }) require.NoError(t, err) + + err = initiatorChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = responderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + + _, err = initiatorChannel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Second), + ObservationPeriodTime: 10, + ObservationPeriodLedgerGap: 10, + }) + require.EqualError(t, err, "cannot propose a new open if channel has already opened") + + _, err = responderChannel.ConfirmOpen(m) + require.EqualError(t, err, "validating open agreement: cannot confirm a new open if channel is already opened") } diff --git a/sdk/state/payment.go b/sdk/state/payment.go index b0f24b09..eab1027a 100644 --- a/sdk/state/payment.go +++ b/sdk/state/payment.go @@ -96,7 +96,11 @@ func (c *Channel) ProposePayment(amount int64) (CloseAgreement, error) { } // If the channel is not open yet, error. - if c.latestAuthorizedCloseAgreement.isEmpty() { + cs, err := c.State() + if err != nil { + return CloseAgreement{}, fmt.Errorf("getting channel state: %w", err) + } + if cs != StateOpen { return CloseAgreement{}, fmt.Errorf("cannot propose a payment before channel is opened") } @@ -158,14 +162,18 @@ var ErrUnderfunded = fmt.Errorf("account is underfunded to make payment") // on the state of the close agreement signatures. func (c *Channel) validatePayment(ca CloseAgreement) (err error) { // If the channel is not open yet, error. - if c.latestAuthorizedCloseAgreement.isEmpty() { + cs, err := c.State() + if err != nil { + return fmt.Errorf("getting channel state: %w", err) + } + if cs < StateOpen { return fmt.Errorf("cannot confirm a payment before channel is opened") } // If a coordinated close has been proposed by this channel already, error. if !c.latestUnauthorizedCloseAgreement.isEmpty() && c.latestUnauthorizedCloseAgreement.Details.ObservationPeriodTime == 0 && c.latestUnauthorizedCloseAgreement.Details.ObservationPeriodLedgerGap == 0 { - return fmt.Errorf("cannot propose payment after proposing a coordinated close") + return fmt.Errorf("cannot confirm payment after proposing a coordinated close") } // If a coordinated close has been accepted already, error. diff --git a/sdk/state/payment_test.go b/sdk/state/payment_test.go index 0e7d47a8..693e9fe4 100644 --- a/sdk/state/payment_test.go +++ b/sdk/state/payment_test.go @@ -5,8 +5,10 @@ import ( "testing" "time" + "github.com/stellar/experimental-payment-channels/sdk/txbuildtest" "github.com/stellar/go/keypair" "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -216,7 +218,7 @@ func TestChannel_ConfirmPayment_acceptsSameObservationPeriod(t *testing.T) { SequenceNumber: int64(202), } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: true, @@ -225,16 +227,47 @@ func TestChannel_ConfirmPayment_acceptsSameObservationPeriod(t *testing.T) { LocalEscrowAccount: localEscrowAccount, RemoteEscrowAccount: remoteEscrowAccount, }) - channel.latestAuthorizedCloseAgreement = CloseAgreement{ - Details: CloseAgreementDetails{ - ObservationPeriodTime: 1, - ObservationPeriodLedgerGap: 1, - }, + + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) } // A close agreement from the remote participant should be accepted if the // observation period matches the channels observation period. { + channel.latestAuthorizedCloseAgreement = CloseAgreement{ + Details: CloseAgreementDetails{ + ObservationPeriodTime: 1, + ObservationPeriodLedgerGap: 1, + ConfirmingSigner: localSigner.FromAddress(), + }, + } + txDecl, txClose, err := channel.closeTxs(channel.openAgreement.Details, CloseAgreementDetails{ IterationNumber: 1, ObservationPeriodTime: 1, @@ -276,7 +309,7 @@ func TestChannel_ConfirmPayment_rejectsDifferentObservationPeriod(t *testing.T) SequenceNumber: int64(202), } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: true, @@ -285,10 +318,41 @@ func TestChannel_ConfirmPayment_rejectsDifferentObservationPeriod(t *testing.T) LocalEscrowAccount: localEscrowAccount, RemoteEscrowAccount: remoteEscrowAccount, }) + + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + channel.latestAuthorizedCloseAgreement = CloseAgreement{ Details: CloseAgreementDetails{ ObservationPeriodTime: 1, ObservationPeriodLedgerGap: 1, + ConfirmingSigner: localSigner.FromAddress(), }, } @@ -333,7 +397,7 @@ func TestChannel_ConfirmPayment_localWhoIsInitiatorRejectsPaymentToRemoteWhoIsRe SequenceNumber: int64(202), } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: true, @@ -343,21 +407,47 @@ func TestChannel_ConfirmPayment_localWhoIsInitiatorRejectsPaymentToRemoteWhoIsRe RemoteEscrowAccount: remoteEscrowAccount, }) + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // A close agreement from the remote participant should be rejected if the // payment changes the balance in the favor of the remote. - channel.openAgreement = OpenAgreement{ - Details: OpenAgreementDetails{ - Asset: NativeAsset, - }, - } channel.latestAuthorizedCloseAgreement = CloseAgreement{ Details: CloseAgreementDetails{ IterationNumber: 1, Balance: 100, // Local (initiator) owes remote (responder) 100. ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, + ConfirmingSigner: localSigner.FromAddress(), }, } + ca := CloseAgreementDetails{ IterationNumber: 2, Balance: 110, // Local (initiator) owes remote (responder) 110, payment of 10 from ❌ local to remote. @@ -394,7 +484,7 @@ func TestChannel_ConfirmPayment_localWhoIsResponderRejectsPaymentToRemoteWhoIsIn SequenceNumber: int64(202), } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: false, @@ -404,19 +494,44 @@ func TestChannel_ConfirmPayment_localWhoIsResponderRejectsPaymentToRemoteWhoIsIn RemoteEscrowAccount: remoteEscrowAccount, }) + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: remoteSigner.Address(), + ResponderSigner: localSigner.Address(), + InitiatorEscrow: remoteEscrowAccount.Address.Address(), + ResponderEscrow: localEscrowAccount.Address.Address(), + StartSequence: remoteEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // A close agreement from the remote participant should be rejected if the // payment changes the balance in the favor of the remote. - channel.openAgreement = OpenAgreement{ - Details: OpenAgreementDetails{ - Asset: NativeAsset, - }, - } channel.latestAuthorizedCloseAgreement = CloseAgreement{ Details: CloseAgreementDetails{ IterationNumber: 1, Balance: 100, // Remote (initiator) owes local (responder) 100. ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, + ConfirmingSigner: localSigner.FromAddress(), }, } ca := CloseAgreementDetails{ @@ -427,6 +542,7 @@ func TestChannel_ConfirmPayment_localWhoIsResponderRejectsPaymentToRemoteWhoIsIn ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, } + txDecl, txClose, err := channel.closeTxs(channel.openAgreement.Details, ca) require.NoError(t, err) txDecl, err = txDecl.Sign(network.TestNetworkPassphrase, remoteSigner) @@ -457,7 +573,7 @@ func TestChannel_ConfirmPayment_initiatorRejectsPaymentThatIsUnderfunded(t *test Balance: 100, } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: true, @@ -467,6 +583,35 @@ func TestChannel_ConfirmPayment_initiatorRejectsPaymentThatIsUnderfunded(t *test RemoteEscrowAccount: remoteEscrowAccount, }) + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // A close agreement from the remote participant should be rejected if the // payment changes the balance in the favor of the remote. channel.latestAuthorizedCloseAgreement = CloseAgreement{ @@ -475,8 +620,10 @@ func TestChannel_ConfirmPayment_initiatorRejectsPaymentThatIsUnderfunded(t *test Balance: -60, // Remote (responder) owes local (initiator) 60. ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, + ConfirmingSigner: localSigner.FromAddress(), }, } + ca := CloseAgreementDetails{ IterationNumber: 2, Balance: -110, // Remote (responder) owes local (initiator) 110, which responder ❌ cannot pay. @@ -527,7 +674,7 @@ func TestChannel_ConfirmPayment_responderRejectsPaymentThatIsUnderfunded(t *test Balance: 100, } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: false, @@ -537,6 +684,35 @@ func TestChannel_ConfirmPayment_responderRejectsPaymentThatIsUnderfunded(t *test RemoteEscrowAccount: remoteEscrowAccount, }) + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: remoteSigner.Address(), + ResponderSigner: localSigner.Address(), + InitiatorEscrow: remoteEscrowAccount.Address.Address(), + ResponderEscrow: localEscrowAccount.Address.Address(), + StartSequence: remoteEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // A close agreement from the remote participant should be rejected if the // payment changes the balance in the favor of the remote. channel.latestAuthorizedCloseAgreement = CloseAgreement{ @@ -545,8 +721,10 @@ func TestChannel_ConfirmPayment_responderRejectsPaymentThatIsUnderfunded(t *test Balance: 60, // Remote (initiator) owes local (responder) 60. ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, + ConfirmingSigner: localSigner.FromAddress(), }, } + ca := CloseAgreementDetails{ IterationNumber: 2, Balance: 110, // Remote (initiator) owes local (responder) 110, which initiator ❌ cannot pay. @@ -597,7 +775,7 @@ func TestChannel_ConfirmPayment_initiatorCannotProposePaymentThatIsUnderfunded(t Balance: 100, } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: true, @@ -607,6 +785,35 @@ func TestChannel_ConfirmPayment_initiatorCannotProposePaymentThatIsUnderfunded(t RemoteEscrowAccount: remoteEscrowAccount, }) + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // A close agreement from the remote participant should be rejected if the // payment changes the balance in the favor of the remote. channel.latestAuthorizedCloseAgreement = CloseAgreement{ @@ -615,8 +822,10 @@ func TestChannel_ConfirmPayment_initiatorCannotProposePaymentThatIsUnderfunded(t Balance: 60, // Local (initiator) owes remote (responder) 60. ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, + ConfirmingSigner: localSigner.FromAddress(), }, } + _, err := channel.ProposePayment(110) assert.EqualError(t, err, "amount over commits: account is underfunded to make payment") assert.ErrorIs(t, err, ErrUnderfunded) @@ -641,7 +850,7 @@ func TestChannel_ConfirmPayment_responderCannotProposePaymentThatIsUnderfunded(t Balance: 100, } - // Given a channel with observation periods set to 1, that is already open. + // Given a channel with observation periods set to 1. channel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, Initiator: false, @@ -651,6 +860,35 @@ func TestChannel_ConfirmPayment_responderCannotProposePaymentThatIsUnderfunded(t RemoteEscrowAccount: remoteEscrowAccount, }) + // Put channel into the Open state. + { + _, err := channel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), + }) + require.NoError(t, err) + + ftx, err := channel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: remoteSigner.Address(), + ResponderSigner: localSigner.Address(), + InitiatorEscrow: remoteEscrowAccount.Address.Address(), + ResponderEscrow: localEscrowAccount.Address.Address(), + StartSequence: remoteEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = channel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // A close agreement from the remote participant should be rejected if the // payment changes the balance in the favor of the remote. channel.latestAuthorizedCloseAgreement = CloseAgreement{ @@ -659,8 +897,10 @@ func TestChannel_ConfirmPayment_responderCannotProposePaymentThatIsUnderfunded(t Balance: -60, // Local (responder) owes remote (initiator) 60. ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, + ConfirmingSigner: localSigner.FromAddress(), }, } + _, err := channel.ProposePayment(110) assert.EqualError(t, err, "amount over commits: account is underfunded to make payment") assert.ErrorIs(t, err, ErrUnderfunded) @@ -677,12 +917,12 @@ func TestLastConfirmedPayment(t *testing.T) { localEscrowAccount := &EscrowAccount{ Address: keypair.MustRandom().FromAddress(), SequenceNumber: int64(101), - Balance: 1000, + Balance: 0, } remoteEscrowAccount := &EscrowAccount{ Address: keypair.MustRandom().FromAddress(), SequenceNumber: int64(202), - Balance: 1000, + Balance: 0, } sendingChannel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, @@ -691,6 +931,7 @@ func TestLastConfirmedPayment(t *testing.T) { RemoteSigner: remoteSigner.FromAddress(), LocalEscrowAccount: localEscrowAccount, RemoteEscrowAccount: remoteEscrowAccount, + MaxOpenExpiry: 2 * time.Hour, }) receiverChannel := NewChannel(Config{ NetworkPassphrase: network.TestNetworkPassphrase, @@ -699,26 +940,50 @@ func TestLastConfirmedPayment(t *testing.T) { RemoteSigner: localSigner.FromAddress(), LocalEscrowAccount: remoteEscrowAccount, RemoteEscrowAccount: localEscrowAccount, + MaxOpenExpiry: 2 * time.Hour, }) - // // latest close agreement should be set during open steps - sendingChannel.latestAuthorizedCloseAgreement = CloseAgreement{ - Details: CloseAgreementDetails{ - IterationNumber: 1, - Balance: 0, - ObservationPeriodTime: 10, - ObservationPeriodLedgerGap: 10, - }, - } - receiverChannel.latestAuthorizedCloseAgreement = CloseAgreement{ - Details: CloseAgreementDetails{ - IterationNumber: 1, - Balance: 0, + // Put channel into the Open state. + { + m, err := sendingChannel.ProposeOpen(OpenParams{ + Asset: NativeAsset, + ExpiresAt: time.Now().Add(5 * time.Minute), ObservationPeriodTime: 10, ObservationPeriodLedgerGap: 10, - }, + }) + require.NoError(t, err) + m, err = receiverChannel.ConfirmOpen(m) + require.NoError(t, err) + _, err = sendingChannel.ConfirmOpen(m) + require.NoError(t, err) + + ftx, err := sendingChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = sendingChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = receiverChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) } + sendingChannel.UpdateLocalEscrowAccountBalance(1000) + sendingChannel.UpdateRemoteEscrowAccountBalance(1000) + + // Test the returned close agreemenets are as expected. ca, err := sendingChannel.ProposePayment(200) require.NoError(t, err) assert.Equal(t, ca, sendingChannel.latestUnauthorizedCloseAgreement) @@ -804,6 +1069,31 @@ func TestChannel_ProposeAndConfirmPayment_rejectIfChannelNotOpen(t *testing.T) { _, err = senderChannel.ConfirmOpen(m) require.NoError(t, err) + // Put channel into the Open state. + { + ftx, err := senderChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = senderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = receiverChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + // Sender proposes coordinated close. ca, err := senderChannel.ProposeClose() require.NoError(t, err) @@ -823,7 +1113,7 @@ func TestChannel_ProposeAndConfirmPayment_rejectIfChannelNotOpen(t *testing.T) { }, } _, err = senderChannel.ConfirmPayment(p) - require.EqualError(t, err, "validating payment: cannot propose payment after proposing a coordinated close") + require.EqualError(t, err, "validating payment: cannot confirm payment after proposing a coordinated close") // Finish close. ca, err = receiverChannel.ConfirmClose(ca) @@ -901,6 +1191,34 @@ func TestChannel_enforceOnlyOneCloseAgreementAllowed(t *testing.T) { _, err = senderChannel.ConfirmOpen(m) require.NoError(t, err) + // Put channel into the Open state. + { + ftx, err := senderChannel.OpenTx() + require.NoError(t, err) + ftxXDR, err := ftx.Base64() + require.NoError(t, err) + + successResultXDR, err := txbuildtest.BuildResultXDR(true) + require.NoError(t, err) + resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{ + InitiatorSigner: localSigner.Address(), + ResponderSigner: remoteSigner.Address(), + InitiatorEscrow: localEscrowAccount.Address.Address(), + ResponderEscrow: remoteEscrowAccount.Address.Address(), + StartSequence: localEscrowAccount.SequenceNumber + 1, + Asset: txnbuild.NativeAsset{}, + }) + require.NoError(t, err) + + err = senderChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + err = receiverChannel.IngestTx(ftxXDR, successResultXDR, resultMetaXDR) + require.NoError(t, err) + } + + senderChannel.UpdateLocalEscrowAccountBalance(1000) + senderChannel.UpdateRemoteEscrowAccountBalance(1000) + caOriginal := senderChannel.latestAuthorizedCloseAgreement // sender proposes payment diff --git a/sdk/txbuildtest/txbuildtest.go b/sdk/txbuildtest/txbuildtest.go index 101080f5..9b2c6299 100644 --- a/sdk/txbuildtest/txbuildtest.go +++ b/sdk/txbuildtest/txbuildtest.go @@ -3,6 +3,7 @@ package txbuildtest import ( "fmt" + "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" ) @@ -57,3 +58,78 @@ func BuildResultMetaXDR(ledgerEntryResults []xdr.LedgerEntryData) (string, error } return tmXDR, nil } + +type FormationResultMetaParams struct { + InitiatorSigner string + ResponderSigner string + InitiatorEscrow string + ResponderEscrow string + StartSequence int64 + Asset txnbuild.Asset +} + +func BuildFormationResultMetaXDR(params FormationResultMetaParams) (string, error) { + led := []xdr.LedgerEntryData{ + { + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress(params.InitiatorEscrow), + SeqNum: xdr.SequenceNumber(params.StartSequence), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner(params.InitiatorSigner), + Weight: 1, + }, + { + Key: xdr.MustSigner(params.ResponderSigner), + Weight: 1, + }, + }, + Thresholds: xdr.Thresholds{0, 2, 2, 2}, + }, + }, + { + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress(params.ResponderEscrow), + SeqNum: xdr.SequenceNumber(1), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner(params.InitiatorSigner), + Weight: 1, + }, + { + Key: xdr.MustSigner(params.ResponderSigner), + Weight: 1, + }, + }, + Thresholds: xdr.Thresholds{0, 2, 2, 2}, + }, + }, + } + + if !params.Asset.IsNative() { + led = append(led, []xdr.LedgerEntryData{ + { + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress(params.InitiatorEscrow), + Balance: 0, + Asset: xdr.MustNewCreditAsset(params.Asset.GetCode(), params.Asset.GetIssuer()), + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + { + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress(params.ResponderEscrow), + Balance: 0, + Asset: xdr.MustNewCreditAsset(params.Asset.GetCode(), params.Asset.GetIssuer()), + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + }...) + } + + return BuildResultMetaXDR(led) +}