diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index 92cb00b3ee..e996f27b09 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -14,6 +14,7 @@ import ( "time" "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" "github.com/manucorporat/sse" @@ -47,7 +48,7 @@ func (c *Client) checkMemoRequired(transaction *txnbuild.Transaction) error { for i, op := range transaction.Operations() { var destination string - if err := op.Validate(); err != nil { + if err := op.Validate(true); err != nil { return err } @@ -64,10 +65,15 @@ func (c *Client) checkMemoRequired(transaction *txnbuild.Transaction) error { continue } - // TODO: once we support M-strkeys (SEP23), also check whether the destination - // is a muxed account with a memo ID. + muxed, err := xdr.AddressToMuxedAccount(destination) + if err != nil { + return errors.Wrapf(err, "destination %v is not a valid address", destination) + } + // Skip destination addresses with a memo id because the address has a memo + // encoded within it + destinationHasMemoID := muxed.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 - if destinations[destination] { + if destinations[destination] || destinationHasMemoID { continue } destinations[destination] = true diff --git a/clients/horizonclient/main_test.go b/clients/horizonclient/main_test.go index 21f1c8c3d6..dc935305a2 100644 --- a/clients/horizonclient/main_test.go +++ b/clients/horizonclient/main_test.go @@ -17,6 +17,8 @@ import ( "github.com/stellar/go/support/errors" "github.com/stellar/go/support/http/httptest" "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" ) @@ -903,6 +905,70 @@ func TestSubmitTransactionRequest(t *testing.T) { assert.Equal(t, ErrAccountRequiresMemo, errors.Cause(err)) } +func TestSubmitTransactionRequestMuxedAccounts(t *testing.T) { + hmock := httptest.NewClient() + client := &Client{ + HorizonURL: "https://localhost/", + HTTP: hmock, + } + + kp := keypair.MustParseFull("SA26PHIKZM6CXDGR472SSGUQQRYXM6S437ZNHZGRM6QA4FOPLLLFRGDX") + accountID := xdr.MustAddress(kp.Address()) + mx := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *accountID.Ed25519, + }, + } + sourceAccount := txnbuild.NewSimpleAccount(mx.Address(), int64(0)) + + payment := txnbuild.Payment{ + Destination: kp.Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + } + + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{&payment}, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewTimebounds(0, 10), + EnableMuxedAccounts: true, + }, + ) + assert.NoError(t, err) + + tx, err = tx.Sign(network.TestNetworkPassphrase, kp) + assert.NoError(t, err) + + // successful tx with config.memo_required not found + hmock.On( + "POST", + "https://localhost/transactions?tx=AAAAAgAAAQAAAAAAyv66vgU08yUQ8sHqhY8j9mXWwERfHC%2F3cKFSe%2FspAr0rGtO2AAAAZAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAQAAAAAAAAABAAAAAAU08yUQ8sHqhY8j9mXWwERfHC%2F3cKFSe%2FspAr0rGtO2AAAAAAAAAAAF9eEAAAAAAAAAAAErGtO2AAAAQJvQkE9UVo%2FmfFBl%2F8ZPTzSUyVO4nvW0BYfnbowoBPEdRfLOLQz28v6sBKQc2b86NUfVHN5TQVo3%2BjH4nK9wVgk%3D", + ).ReturnString(200, txSuccess) + + hmock.On( + "GET", + "https://localhost/accounts/GACTJ4ZFCDZMD2UFR4R7MZOWYBCF6HBP65YKCUT37MUQFPJLDLJ3N5D2/data/config.memo_required", + ).ReturnString(404, notFoundResponse) + + _, err = client.SubmitTransaction(tx) + assert.NoError(t, err) + + // memo required - does not submit transaction + hmock.On( + "GET", + "https://localhost/accounts/GACTJ4ZFCDZMD2UFR4R7MZOWYBCF6HBP65YKCUT37MUQFPJLDLJ3N5D2/data/config.memo_required", + ).ReturnJSON(200, memoRequiredResponse) + + _, err = client.SubmitTransaction(tx) + assert.Error(t, err) + assert.Equal(t, ErrAccountRequiresMemo, errors.Cause(err)) +} + func TestSubmitFeeBumpTransaction(t *testing.T) { hmock := httptest.NewClient() client := &Client{ diff --git a/strkey/decode_test.go b/strkey/decode_test.go index 5b8bc7b24d..431a87d287 100644 --- a/strkey/decode_test.go +++ b/strkey/decode_test.go @@ -24,6 +24,18 @@ func TestDecode(t *testing.T) { 0xec, 0x9c, 0x07, 0x3d, 0x05, 0xc7, 0xb1, 0x03, }, }, + { + Name: "MuxedAccount", + Address: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + ExpectedVersionByte: VersionByteMuxedAccount, + ExpectedPayload: []byte{ + 0x3f, 0x0c, 0x34, 0xbf, 0x93, 0xad, 0x0d, 0x99, + 0x71, 0xd0, 0x4c, 0xcc, 0x90, 0xf7, 0x05, 0x51, + 0x1c, 0x83, 0x8a, 0xad, 0x97, 0x34, 0xa4, 0xa2, + 0xfb, 0x0d, 0x7a, 0x03, 0xfc, 0x7f, 0xe8, 0x9a, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, { Name: "Seed", Address: "SBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWHOKR", @@ -85,6 +97,83 @@ func TestDecode(t *testing.T) { // corrupted payload _, err = Decode(VersionByteAccountID, "GA3D5KRYM6CB7OWOOOORR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQHES5") assert.Error(t, err) + + // non-canonical representation due to extra character + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLKA") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused leftover character") + } + + // non-canonical representation due to leftover bits set to 1 (some of the test strkeys are too short for a muxed account + // but they comply with the test's purpose all the same) + + // 1 unused bit (length 69) + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLH") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUR") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // 4 unused bits (length 68) + + // 'B' is equivalent to 0b00001 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJB") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // 'C' is equivalent to 0b00010 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJC") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // 'E' is equivalent to 0b00100 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJE") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // 'I' is equivalent to 0b01000 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJI") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // '7' is equivalent to 0b11111 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJ7") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // '6' is equivalent to 0b11110 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJ6") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // '4' is equivalent to 0b11100 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJ4") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + // 'Y' is equivalent to 0b11000 + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJY") + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unused bits should be set to 0") + } + + // 'Q' is equivalent to 0b10000, so there should be no error + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJQ") + assert.NotContains(t, err.Error(), "unused bits should be set to 0") + + // Padding bytes are not allowed + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK===") + assert.Contains(t, err.Error(), "illegal base32 data") + + // Invalid algorithm (low 3 bits of version byte are 7) + _, err = Decode(VersionByteMuxedAccount, "M47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ") + assert.Contains(t, err.Error(), "invalid version byte") + + // Invalid checksum + _, err = Decode(VersionByteMuxedAccount, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUO") + assert.Contains(t, err.Error(), "invalid checksum") } func TestMalformed(t *testing.T) { diff --git a/strkey/encode_test.go b/strkey/encode_test.go index b020074406..141dc0c8a2 100644 --- a/strkey/encode_test.go +++ b/strkey/encode_test.go @@ -24,6 +24,18 @@ func TestEncode(t *testing.T) { }, Expected: "GA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQHES5", }, + { + Name: "MuxedAccount", + VersionByte: VersionByteMuxedAccount, + Payload: []byte{ + 0x3f, 0x0c, 0x34, 0xbf, 0x93, 0xad, 0x0d, 0x99, + 0x71, 0xd0, 0x4c, 0xcc, 0x90, 0xf7, 0x05, 0x51, + 0x1c, 0x83, 0x8a, 0xad, 0x97, 0x34, 0xa4, 0xa2, + 0xfb, 0x0d, 0x7a, 0x03, 0xfc, 0x7f, 0xe8, 0x9a, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + Expected: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + }, { Name: "Seed", VersionByte: VersionByteSeed, diff --git a/strkey/main.go b/strkey/main.go index 167efaa4ee..6facce6e2c 100644 --- a/strkey/main.go +++ b/strkey/main.go @@ -24,6 +24,9 @@ const ( //VersionByteSeed is the version byte used for encoded stellar seed VersionByteSeed = 18 << 3 // Base32-encodes to 'S...' + //VersionByteMuxedAccounts is the version byte used for encoded stellar multiplexed addresses + VersionByteMuxedAccount = 12 << 3 // Base32-encodes to 'M...' + //VersionByteHashTx is the version byte used for encoded stellar hashTx //signer keys. VersionByteHashTx = 19 << 3 // Base32-encodes to 'T...' @@ -161,9 +164,7 @@ func Version(src string) (VersionByte, error) { // is not one of the defined valid version byte constants. func checkValidVersionByte(version VersionByte) error { switch version { - // intentionally disallow M-strkeys (versionByteMuxedAccount) - // until SEP23 leaves the Draft status. - case VersionByteAccountID, VersionByteSeed, VersionByteHashTx, VersionByteHashX: + case VersionByteAccountID, VersionByteMuxedAccount, VersionByteSeed, VersionByteHashTx, VersionByteHashX: return nil default: return ErrInvalidVersionByte diff --git a/txnbuild/CHANGELOG.md b/txnbuild/CHANGELOG.md index f2ae1ef1d0..094765565e 100644 --- a/txnbuild/CHANGELOG.md +++ b/txnbuild/CHANGELOG.md @@ -8,11 +8,16 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking changes * `AllowTrustOpAsset` was renamed to `AssetCode`, `{Must}NewAllowTrustAsset` was renamed to `{Must}NewAssetCodeFromString`. +* Some methods from the `Operation` interface (`BuildXDR()`,`FromXDR()` and `Validate()`) now take an additional `bool` parameter (`withMuxedAccounts`) + to indicate whether [SEP23](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md) M-strkeys should be enabled. ### New features -Add support for Stellar Protocol 16 (CAP35): `Clawback` operations. - +* Add support for Stellar Protocol 17 (CAP35): `Clawback`, `ClawbackClaimableBalance` and `SetTrustlineFlags` operations. +* Add opt-in support for [SEP23](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md) M-strkeys for `MuxedAccount`s: + * Some methods from the `Operation` interface (`BuildXDR()`,`FromXDR()` and `Validate()`) now take an additional `bool` parameter (`withMuxedAccounts`) + * The parameters from `NewFeeBumpTransaction()` and `NewTransaction()` now include a new field (`EnableMuxedAccounts`) to enable M-strekeys. + * `TransactionFromXDR()` now allows passing a `TransactionFromXDROptionEnableMuxedAccounts` option, to enable M-strkey parsing. ## [v6.0.0](https://github.com/stellar/go/releases/tag/horizonclient-v6.0.0) - 2021-02-22 ### Breaking changes diff --git a/txnbuild/account_merge.go b/txnbuild/account_merge.go index 147b85216a..ddee989b4c 100644 --- a/txnbuild/account_merge.go +++ b/txnbuild/account_merge.go @@ -13,10 +13,14 @@ type AccountMerge struct { } // BuildXDR for AccountMerge returns a fully configured XDR Operation. -func (am *AccountMerge) BuildXDR() (xdr.Operation, error) { +func (am *AccountMerge) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var xdrOp xdr.MuxedAccount - - err := xdrOp.SetAddress(am.Destination) + var err error + if withMuxedAccounts { + err = xdrOp.SetAddress(am.Destination) + } else { + err = xdrOp.SetEd25519Address(am.Destination) + } if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set destination address") } @@ -27,20 +31,28 @@ func (am *AccountMerge) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, am.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, am.SourceAccount) + } else { + SetOpSourceAccount(&op, am.SourceAccount) + } return op, nil } // FromXDR for AccountMerge initialises the txnbuild struct from the corresponding xdr Operation. -func (am *AccountMerge) FromXDR(xdrOp xdr.Operation) error { +func (am *AccountMerge) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { if xdrOp.Body.Type != xdr.OperationTypeAccountMerge { return errors.New("error parsing account_merge operation from xdr") } - am.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + am.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) if xdrOp.Body.Destination != nil { - aid := xdrOp.Body.Destination.ToAccountId() - am.Destination = aid.Address() + if withMuxedAccounts { + am.Destination = xdrOp.Body.Destination.Address() + } else { + aid := xdrOp.Body.Destination.ToAccountId() + am.Destination = aid.Address() + } } return nil @@ -48,8 +60,13 @@ func (am *AccountMerge) FromXDR(xdrOp xdr.Operation) error { // Validate for AccountMerge validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (am *AccountMerge) Validate() error { - _, err := xdr.AddressToAccountId(am.Destination) +func (am *AccountMerge) Validate(withMuxedAccounts bool) error { + var err error + if withMuxedAccounts { + _, err = xdr.AddressToAccountId(am.Destination) + } else { + _, err = xdr.AddressToMuxedAccount(am.Destination) + } if err != nil { return NewValidationError("Destination", err.Error()) } diff --git a/txnbuild/account_merge_test.go b/txnbuild/account_merge_test.go index ebf002526d..7d1e21ff32 100644 --- a/txnbuild/account_merge_test.go +++ b/txnbuild/account_merge_test.go @@ -23,7 +23,21 @@ func TestAccountMergeValidate(t *testing.T) { }, ) if assert.Error(t, err) { - expected := "strkey is 4 bytes long; minimum valid length is 5" + expected := "invalid address" assert.Contains(t, err.Error(), expected) } } + +func TestAccountMergeRoundtrip(t *testing.T) { + accountMerge := AccountMerge{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Destination: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + } + testOperationsMarshallingRoundtrip(t, []Operation{&accountMerge}, false) + + accountMerge = AccountMerge{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Destination: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + } + testOperationsMarshallingRoundtrip(t, []Operation{&accountMerge}, true) +} diff --git a/txnbuild/allow_trust.go b/txnbuild/allow_trust.go index 6bb22d97fc..f616a585ca 100644 --- a/txnbuild/allow_trust.go +++ b/txnbuild/allow_trust.go @@ -21,7 +21,7 @@ type AllowTrust struct { } // BuildXDR for AllowTrust returns a fully configured XDR Operation. -func (at *AllowTrust) BuildXDR() (xdr.Operation, error) { +func (at *AllowTrust) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var xdrOp xdr.AllowTrustOp // Set XDR address associated with the trustline @@ -56,7 +56,11 @@ func (at *AllowTrust) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, at.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, at.SourceAccount) + } else { + SetOpSourceAccount(&op, at.SourceAccount) + } return op, nil } @@ -75,13 +79,13 @@ func assetCodeToCreditAsset(assetCode xdr.AssetCode) (CreditAsset, error) { } // FromXDR for AllowTrust initialises the txnbuild struct from the corresponding xdr Operation. -func (at *AllowTrust) FromXDR(xdrOp xdr.Operation) error { +func (at *AllowTrust) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetAllowTrustOp() if !ok { return errors.New("error parsing allow_trust operation from xdr") } - at.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + at.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) at.Trustor = result.Trustor.Address() flag := xdr.TrustLineFlags(result.Authorize) at.Authorize = flag.IsAuthorized() @@ -97,7 +101,7 @@ func (at *AllowTrust) FromXDR(xdrOp xdr.Operation) error { // Validate for AllowTrust validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (at *AllowTrust) Validate() error { +func (at *AllowTrust) Validate(withMuxedAccounts bool) error { err := validateStellarPublicKey(at.Trustor) if err != nil { return NewValidationError("Trustor", err.Error()) diff --git a/txnbuild/allow_trust_test.go b/txnbuild/allow_trust_test.go index 4800a82beb..607e58a81b 100644 --- a/txnbuild/allow_trust_test.go +++ b/txnbuild/allow_trust_test.go @@ -57,3 +57,22 @@ func TestAllowTrustValidateTrustor(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestAllowTrustRoundtrip(t *testing.T) { + allowTrust := AllowTrust{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Trustor: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Type: CreditAsset{"USD", ""}, + Authorize: true, + } + testOperationsMarshallingRoundtrip(t, []Operation{&allowTrust}, false) + + // with muxed accounts + allowTrust = AllowTrust{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Trustor: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Type: CreditAsset{"USD", ""}, + Authorize: true, + } + testOperationsMarshallingRoundtrip(t, []Operation{&allowTrust}, true) +} diff --git a/txnbuild/begin_sponsoring_future_reserves.go b/txnbuild/begin_sponsoring_future_reserves.go index c4a551389f..e4a383258c 100644 --- a/txnbuild/begin_sponsoring_future_reserves.go +++ b/txnbuild/begin_sponsoring_future_reserves.go @@ -15,7 +15,7 @@ type BeginSponsoringFutureReserves struct { } // BuildXDR for BeginSponsoringFutureReserves returns a fully configured XDR Operation. -func (bs *BeginSponsoringFutureReserves) BuildXDR() (xdr.Operation, error) { +func (bs *BeginSponsoringFutureReserves) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrOp := xdr.BeginSponsoringFutureReservesOp{} err := xdrOp.SponsoredId.SetAddress(bs.SponsoredID) if err != nil { @@ -32,12 +32,12 @@ func (bs *BeginSponsoringFutureReserves) BuildXDR() (xdr.Operation, error) { } // FromXDR for BeginSponsoringFutureReserves initializes the txnbuild struct from the corresponding xdr Operation. -func (bs *BeginSponsoringFutureReserves) FromXDR(xdrOp xdr.Operation) error { +func (bs *BeginSponsoringFutureReserves) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetBeginSponsoringFutureReservesOp() if !ok { return errors.New("error parsing begin_sponsoring_future_reserves operation from xdr") } - bs.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + bs.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) bs.SponsoredID = result.SponsoredId.Address() return nil @@ -45,7 +45,7 @@ func (bs *BeginSponsoringFutureReserves) FromXDR(xdrOp xdr.Operation) error { // Validate for BeginSponsoringFutureReserves validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (bs *BeginSponsoringFutureReserves) Validate() error { +func (bs *BeginSponsoringFutureReserves) Validate(withMuxedAccounts bool) error { err := validateStellarPublicKey(bs.SponsoredID) if err != nil { return NewValidationError("SponsoredID", err.Error()) diff --git a/txnbuild/begin_sponsoring_future_reserves_test.go b/txnbuild/begin_sponsoring_future_reserves_test.go index bd93863610..bcacb96c1d 100644 --- a/txnbuild/begin_sponsoring_future_reserves_test.go +++ b/txnbuild/begin_sponsoring_future_reserves_test.go @@ -9,5 +9,5 @@ func TestBeginSponsoringFutureReservesRoundTrip(t *testing.T) { SponsoredID: newKeypair1().Address(), } - testOperationsMarshallingRoundtrip(t, []Operation{beginSponsoring}) + testOperationsMarshallingRoundtrip(t, []Operation{beginSponsoring}, false) } diff --git a/txnbuild/bump_sequence.go b/txnbuild/bump_sequence.go index 09def74c4d..61e32acd28 100644 --- a/txnbuild/bump_sequence.go +++ b/txnbuild/bump_sequence.go @@ -13,7 +13,7 @@ type BumpSequence struct { } // BuildXDR for BumpSequence returns a fully configured XDR Operation. -func (bs *BumpSequence) BuildXDR() (xdr.Operation, error) { +func (bs *BumpSequence) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { opType := xdr.OperationTypeBumpSequence xdrOp := xdr.BumpSequenceOp{BumpTo: xdr.SequenceNumber(bs.BumpTo)} body, err := xdr.NewOperationBody(opType, xdrOp) @@ -21,25 +21,29 @@ func (bs *BumpSequence) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, bs.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, bs.SourceAccount) + } else { + SetOpSourceAccount(&op, bs.SourceAccount) + } return op, nil } // FromXDR for BumpSequence initialises the txnbuild struct from the corresponding xdr Operation. -func (bs *BumpSequence) FromXDR(xdrOp xdr.Operation) error { +func (bs *BumpSequence) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetBumpSequenceOp() if !ok { return errors.New("error parsing bump_sequence operation from xdr") } - bs.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + bs.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) bs.BumpTo = int64(result.BumpTo) return nil } // Validate for BumpSequence validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (bs *BumpSequence) Validate() error { +func (bs *BumpSequence) Validate(withMuxedAccounts bool) error { err := validateAmount(bs.BumpTo) if err != nil { return NewValidationError("BumpTo", err.Error()) diff --git a/txnbuild/bump_sequence_test.go b/txnbuild/bump_sequence_test.go index 3f8d470fed..067cdd0b8f 100644 --- a/txnbuild/bump_sequence_test.go +++ b/txnbuild/bump_sequence_test.go @@ -27,3 +27,17 @@ func TestBumpSequenceValidate(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestBumpSequenceRountrip(t *testing.T) { + bumpSequence := BumpSequence{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + BumpTo: 10, + } + testOperationsMarshallingRoundtrip(t, []Operation{&bumpSequence}, false) + + bumpSequence = BumpSequence{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + BumpTo: 10, + } + testOperationsMarshallingRoundtrip(t, []Operation{&bumpSequence}, true) +} diff --git a/txnbuild/change_trust.go b/txnbuild/change_trust.go index 8efa08902b..d276d302c1 100644 --- a/txnbuild/change_trust.go +++ b/txnbuild/change_trust.go @@ -30,7 +30,7 @@ func RemoveTrustlineOp(issuedAsset Asset) ChangeTrust { } // BuildXDR for ChangeTrust returns a fully configured XDR Operation. -func (ct *ChangeTrust) BuildXDR() (xdr.Operation, error) { +func (ct *ChangeTrust) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { if ct.Line.IsNative() { return xdr.Operation{}, errors.New("trustline cannot be extended to a native (XLM) asset") } @@ -58,18 +58,22 @@ func (ct *ChangeTrust) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, ct.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, ct.SourceAccount) + } else { + SetOpSourceAccount(&op, ct.SourceAccount) + } return op, nil } // FromXDR for ChangeTrust initialises the txnbuild struct from the corresponding xdr Operation. -func (ct *ChangeTrust) FromXDR(xdrOp xdr.Operation) error { +func (ct *ChangeTrust) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetChangeTrustOp() if !ok { return errors.New("error parsing change_trust operation from xdr") } - ct.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + ct.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) ct.Limit = amount.String(result.Limit) asset, err := assetFromXDR(result.Line) if err != nil { @@ -81,7 +85,7 @@ func (ct *ChangeTrust) FromXDR(xdrOp xdr.Operation) error { // Validate for ChangeTrust validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (ct *ChangeTrust) Validate() error { +func (ct *ChangeTrust) Validate(withMuxedAccounts bool) error { // only validate limit if it has a value. Empty limit is set to the max trustline limit. if ct.Limit != "" { err := validateAmount(ct.Limit) diff --git a/txnbuild/change_trust_test.go b/txnbuild/change_trust_test.go index b33ad41f39..245d3caf06 100644 --- a/txnbuild/change_trust_test.go +++ b/txnbuild/change_trust_test.go @@ -78,3 +78,20 @@ func TestChangeTrustValidateInvalidLimit(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestChangeTrustRoundtrip(t *testing.T) { + changeTrust := ChangeTrust{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Line: CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + Limit: "1.0000000", + } + testOperationsMarshallingRoundtrip(t, []Operation{&changeTrust}, false) + + // with muxed accounts + changeTrust = ChangeTrust{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Line: CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + Limit: "1.0000000", + } + testOperationsMarshallingRoundtrip(t, []Operation{&changeTrust}, true) +} diff --git a/txnbuild/claim_claimable_balance.go b/txnbuild/claim_claimable_balance.go index f9d9d442e0..4f39eb9141 100644 --- a/txnbuild/claim_claimable_balance.go +++ b/txnbuild/claim_claimable_balance.go @@ -15,7 +15,7 @@ type ClaimClaimableBalance struct { } // BuildXDR for ClaimClaimableBalance returns a fully configured XDR Operation. -func (cb *ClaimClaimableBalance) BuildXDR() (xdr.Operation, error) { +func (cb *ClaimClaimableBalance) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var xdrBalanceID xdr.ClaimableBalanceId err := xdr.SafeUnmarshalHex(cb.BalanceID, &xdrBalanceID) if err != nil { @@ -31,18 +31,22 @@ func (cb *ClaimClaimableBalance) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, cb.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, cb.SourceAccount) + } else { + SetOpSourceAccount(&op, cb.SourceAccount) + } return op, nil } // FromXDR for ClaimClaimableBalance initializes the txnbuild struct from the corresponding xdr Operation. -func (cb *ClaimClaimableBalance) FromXDR(xdrOp xdr.Operation) error { +func (cb *ClaimClaimableBalance) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetClaimClaimableBalanceOp() if !ok { return errors.New("error parsing claim_claimable_balance operation from xdr") } - cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) balanceID, err := xdr.MarshalHex(result.BalanceId) if err != nil { return errors.New("error parsing BalanceID in claim_claimable_balance operation from xdr") @@ -54,7 +58,7 @@ func (cb *ClaimClaimableBalance) FromXDR(xdrOp xdr.Operation) error { // Validate for ClaimClaimableBalance validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (cb *ClaimClaimableBalance) Validate() error { +func (cb *ClaimClaimableBalance) Validate(withMuxedAccounts bool) error { var xdrBalanceID xdr.ClaimableBalanceId err := xdr.SafeUnmarshalHex(cb.BalanceID, &xdrBalanceID) if err != nil { diff --git a/txnbuild/claim_claimable_balance_test.go b/txnbuild/claim_claimable_balance_test.go index 5de9d25aa0..1a1e285292 100644 --- a/txnbuild/claim_claimable_balance_test.go +++ b/txnbuild/claim_claimable_balance_test.go @@ -6,8 +6,17 @@ import ( func TestClaimClaimableBalanceRoundTrip(t *testing.T) { claimClaimableBalance := &ClaimClaimableBalance{ - BalanceID: "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfdf5491072", + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + BalanceID: "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfdf5491072", } - testOperationsMarshallingRoundtrip(t, []Operation{claimClaimableBalance}) + testOperationsMarshallingRoundtrip(t, []Operation{claimClaimableBalance}, false) + + // with muxed accounts + claimClaimableBalance = &ClaimClaimableBalance{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + BalanceID: "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfdf5491072", + } + + testOperationsMarshallingRoundtrip(t, []Operation{claimClaimableBalance}, true) } diff --git a/txnbuild/clawback.go b/txnbuild/clawback.go index a19e91743a..7538b5aae9 100644 --- a/txnbuild/clawback.go +++ b/txnbuild/clawback.go @@ -17,10 +17,14 @@ type Clawback struct { } // BuildXDR for Clawback returns a fully configured XDR Operation. -func (cb *Clawback) BuildXDR() (xdr.Operation, error) { +func (cb *Clawback) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var fromMuxedAccount xdr.MuxedAccount - - err := fromMuxedAccount.SetAddress(cb.From) + var err error + if withMuxedAccounts { + err = fromMuxedAccount.SetAddress(cb.From) + } else { + err = fromMuxedAccount.SetEd25519Address(cb.From) + } if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set from address") } @@ -54,20 +58,23 @@ func (cb *Clawback) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR Operation") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, cb.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, cb.SourceAccount) + } else { + SetOpSourceAccount(&op, cb.SourceAccount) + } return op, nil } // FromXDR for Clawback initialises the txnbuild struct from the corresponding xdr Operation. -func (cb *Clawback) FromXDR(xdrOp xdr.Operation) error { +func (cb *Clawback) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetClawbackOp() if !ok { return errors.New("error parsing clawback operation from xdr") } - cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount) - fromAID := result.From.ToAccountId() - cb.From = fromAID.Address() + cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) + cb.From = accountFromXDR(&result.From, withMuxedAccounts) cb.Amount = amount.String(result.Amount) asset, err := assetFromXDR(result.Asset) if err != nil { @@ -80,8 +87,13 @@ func (cb *Clawback) FromXDR(xdrOp xdr.Operation) error { // Validate for Clawback validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (cb *Clawback) Validate() error { - _, err := xdr.AddressToAccountId(cb.From) +func (cb *Clawback) Validate(withMuxedAccounts bool) error { + var err error + if withMuxedAccounts { + _, err = xdr.AddressToMuxedAccount(cb.From) + } else { + _, err = xdr.AddressToAccountId(cb.From) + } if err != nil { return NewValidationError("From", err.Error()) } diff --git a/txnbuild/clawback_claimable_balance.go b/txnbuild/clawback_claimable_balance.go index 4188ac7c3d..da289233a5 100644 --- a/txnbuild/clawback_claimable_balance.go +++ b/txnbuild/clawback_claimable_balance.go @@ -14,7 +14,7 @@ type ClawbackClaimableBalance struct { } // BuildXDR for ClawbackClaimableBalance returns a fully configured XDR Operation. -func (cb *ClawbackClaimableBalance) BuildXDR() (xdr.Operation, error) { +func (cb *ClawbackClaimableBalance) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var xdrBalanceID xdr.ClaimableBalanceId err := xdr.SafeUnmarshalHex(cb.BalanceID, &xdrBalanceID) if err != nil { @@ -30,18 +30,22 @@ func (cb *ClawbackClaimableBalance) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, cb.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, cb.SourceAccount) + } else { + SetOpSourceAccount(&op, cb.SourceAccount) + } return op, nil } // FromXDR for ClawbackClaimableBalance initializes the txnbuild struct from the corresponding xdr Operation. -func (cb *ClawbackClaimableBalance) FromXDR(xdrOp xdr.Operation) error { +func (cb *ClawbackClaimableBalance) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetClawbackClaimableBalanceOp() if !ok { return errors.New("error parsing clawback_claimable_balance operation from xdr") } - cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) balanceID, err := xdr.MarshalHex(result.BalanceId) if err != nil { return errors.New("error parsing BalanceID in claim_claimable_balance operation from xdr") @@ -53,7 +57,7 @@ func (cb *ClawbackClaimableBalance) FromXDR(xdrOp xdr.Operation) error { // Validate for ClawbackClaimableBalance validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (cb *ClawbackClaimableBalance) Validate() error { +func (cb *ClawbackClaimableBalance) Validate(withMuxedAccounts bool) error { var xdrBalanceID xdr.ClaimableBalanceId err := xdr.SafeUnmarshalHex(cb.BalanceID, &xdrBalanceID) if err != nil { diff --git a/txnbuild/clawback_claimable_balance_test.go b/txnbuild/clawback_claimable_balance_test.go index fd699a8637..9ff2914a0e 100644 --- a/txnbuild/clawback_claimable_balance_test.go +++ b/txnbuild/clawback_claimable_balance_test.go @@ -6,8 +6,17 @@ import ( func TestClawbackClaimableBalanceRoundTrip(t *testing.T) { claimClaimableBalance := &ClawbackClaimableBalance{ - BalanceID: "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfdf5491072", + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + BalanceID: "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfdf5491072", } - testOperationsMarshallingRoundtrip(t, []Operation{claimClaimableBalance}) + testOperationsMarshallingRoundtrip(t, []Operation{claimClaimableBalance}, false) + + // with muxed accounts + claimClaimableBalance = &ClawbackClaimableBalance{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + BalanceID: "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfdf5491072", + } + + testOperationsMarshallingRoundtrip(t, []Operation{claimClaimableBalance}, false) } diff --git a/txnbuild/clawback_test.go b/txnbuild/clawback_test.go index a7e329b997..dab757c5a3 100644 --- a/txnbuild/clawback_test.go +++ b/txnbuild/clawback_test.go @@ -83,10 +83,19 @@ func TestClawbackValidateAsset(t *testing.T) { func TestClawbackRoundTrip(t *testing.T) { clawback := Clawback{ - From: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", - Amount: "10.0000000", - Asset: CreditAsset{"USD", "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}, + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + From: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Amount: "10.0000000", + Asset: CreditAsset{"USD", "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}, } + testOperationsMarshallingRoundtrip(t, []Operation{&clawback}, false) - testOperationsMarshallingRoundtrip(t, []Operation{&clawback}) + // with muxed accounts + clawback = Clawback{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + From: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Amount: "10.0000000", + Asset: CreditAsset{"USD", "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&clawback}, true) } diff --git a/txnbuild/create_account.go b/txnbuild/create_account.go index 02e8b3e17d..abbfdacfa3 100644 --- a/txnbuild/create_account.go +++ b/txnbuild/create_account.go @@ -15,7 +15,7 @@ type CreateAccount struct { } // BuildXDR for CreateAccount returns a fully configured XDR Operation. -func (ca *CreateAccount) BuildXDR() (xdr.Operation, error) { +func (ca *CreateAccount) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var xdrOp xdr.CreateAccountOp err := xdrOp.Destination.SetAddress(ca.Destination) @@ -34,18 +34,22 @@ func (ca *CreateAccount) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, ca.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, ca.SourceAccount) + } else { + SetOpSourceAccount(&op, ca.SourceAccount) + } return op, nil } // FromXDR for CreateAccount initialises the txnbuild struct from the corresponding xdr Operation. -func (ca *CreateAccount) FromXDR(xdrOp xdr.Operation) error { +func (ca *CreateAccount) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetCreateAccountOp() if !ok { return errors.New("error parsing create_account operation from xdr") } - ca.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + ca.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) ca.Destination = result.Destination.Address() ca.Amount = amount.String(result.StartingBalance) @@ -54,7 +58,7 @@ func (ca *CreateAccount) FromXDR(xdrOp xdr.Operation) error { // Validate for CreateAccount validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (ca *CreateAccount) Validate() error { +func (ca *CreateAccount) Validate(withMuxedAccounts bool) error { err := validateStellarPublicKey(ca.Destination) if err != nil { return NewValidationError("Destination", err.Error()) diff --git a/txnbuild/create_account_test.go b/txnbuild/create_account_test.go index 0e941d0068..b715dca70d 100644 --- a/txnbuild/create_account_test.go +++ b/txnbuild/create_account_test.go @@ -54,3 +54,21 @@ func TestCreateAccountValidateAmount(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestCreateAccountRoundtrip(t *testing.T) { + createAccount := CreateAccount{ + SourceAccount: "GDYNXQFHU6W5RBW2CCCDDAAU3TMTSU2RMGIBM6HGHAR4NJJKY3IJETHT", + Destination: "GDYNXQFHU6W5RBW2CCCDDAAU3TMTSU2RMGIBM6HGHAR4NJJKY3IJETHT", + Amount: "1.0000000", + } + testOperationsMarshallingRoundtrip(t, []Operation{&createAccount}, false) + + // with muxed accounts + createAccount = CreateAccount{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Destination: "GDYNXQFHU6W5RBW2CCCDDAAU3TMTSU2RMGIBM6HGHAR4NJJKY3IJETHT", + Amount: "1.0000000", + } + testOperationsMarshallingRoundtrip(t, []Operation{&createAccount}, true) + +} diff --git a/txnbuild/create_claimable_balance.go b/txnbuild/create_claimable_balance.go index 812102cb43..578fb8931b 100644 --- a/txnbuild/create_claimable_balance.go +++ b/txnbuild/create_claimable_balance.go @@ -96,7 +96,7 @@ func BeforeRelativeTimePredicate(secondsBefore int64) xdr.ClaimPredicate { } // BuildXDR for CreateClaimableBalance returns a fully configured XDR Operation. -func (cb *CreateClaimableBalance) BuildXDR() (xdr.Operation, error) { +func (cb *CreateClaimableBalance) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrAsset, err := cb.Asset.ToXDR() if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set XDR 'Asset' field") @@ -134,18 +134,22 @@ func (cb *CreateClaimableBalance) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, cb.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, cb.SourceAccount) + } else { + SetOpSourceAccount(&op, cb.SourceAccount) + } return op, nil } // FromXDR for CreateClaimableBalance initializes the txnbuild struct from the corresponding xdr Operation. -func (cb *CreateClaimableBalance) FromXDR(xdrOp xdr.Operation) error { +func (cb *CreateClaimableBalance) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetCreateClaimableBalanceOp() if !ok { return errors.New("error parsing create_claimable_balance operation from xdr") } - cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + cb.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) for _, c := range result.Claimants { claimant := c.MustV0() cb.Destinations = append(cb.Destinations, Claimant{ @@ -166,7 +170,7 @@ func (cb *CreateClaimableBalance) FromXDR(xdrOp xdr.Operation) error { // Validate for CreateClaimableBalance validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (cb *CreateClaimableBalance) Validate() error { +func (cb *CreateClaimableBalance) Validate(withMuxedAccounts bool) error { for _, d := range cb.Destinations { err := validateStellarPublicKey(d.Destination) if err != nil { diff --git a/txnbuild/create_claimable_balance_test.go b/txnbuild/create_claimable_balance_test.go index 49e95b9c64..d6516c7983 100644 --- a/txnbuild/create_claimable_balance_test.go +++ b/txnbuild/create_claimable_balance_test.go @@ -34,7 +34,19 @@ func TestCreateClaimableBalanceRoundTrip(t *testing.T) { }, } - testOperationsMarshallingRoundtrip(t, []Operation{createNativeBalance, createAssetBalance}) + testOperationsMarshallingRoundtrip(t, []Operation{createNativeBalance, createAssetBalance}, false) + + createNativeBalanceWithMuxedAcounts := &CreateClaimableBalance{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Amount: "1234.0000000", + Asset: NativeAsset{}, + Destinations: []Claimant{ + NewClaimant(newKeypair1().Address(), &UnconditionalPredicate), + NewClaimant(newKeypair1().Address(), &and), + }, + } + + testOperationsMarshallingRoundtrip(t, []Operation{createNativeBalanceWithMuxedAcounts}, true) } func TestClaimableBalanceID(t *testing.T) { diff --git a/txnbuild/create_passive_offer.go b/txnbuild/create_passive_offer.go index 1eaba8e964..b77fb23c56 100644 --- a/txnbuild/create_passive_offer.go +++ b/txnbuild/create_passive_offer.go @@ -18,7 +18,7 @@ type CreatePassiveSellOffer struct { } // BuildXDR for CreatePassiveSellOffer returns a fully configured XDR Operation. -func (cpo *CreatePassiveSellOffer) BuildXDR() (xdr.Operation, error) { +func (cpo *CreatePassiveSellOffer) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrSelling, err := cpo.Selling.ToXDR() if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set XDR 'Selling' field") @@ -51,18 +51,22 @@ func (cpo *CreatePassiveSellOffer) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, cpo.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, cpo.SourceAccount) + } else { + SetOpSourceAccount(&op, cpo.SourceAccount) + } return op, nil } // FromXDR for CreatePassiveSellOffer initialises the txnbuild struct from the corresponding xdr Operation. -func (cpo *CreatePassiveSellOffer) FromXDR(xdrOp xdr.Operation) error { +func (cpo *CreatePassiveSellOffer) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetCreatePassiveSellOfferOp() if !ok { return errors.New("error parsing create_passive_sell_offer operation from xdr") } - cpo.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + cpo.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) cpo.Amount = amount.String(result.Amount) if result.Price != (xdr.Price{}) { cpo.price.fromXDR(result.Price) @@ -84,7 +88,7 @@ func (cpo *CreatePassiveSellOffer) FromXDR(xdrOp xdr.Operation) error { // Validate for CreatePassiveSellOffer validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (cpo *CreatePassiveSellOffer) Validate() error { +func (cpo *CreatePassiveSellOffer) Validate(withMuxedAccounts bool) error { return validatePassiveOffer(cpo.Buying, cpo.Selling, cpo.Amount, cpo.Price) } diff --git a/txnbuild/create_passive_offer_test.go b/txnbuild/create_passive_offer_test.go index 3b158d6987..0de8e0ba00 100644 --- a/txnbuild/create_passive_offer_test.go +++ b/txnbuild/create_passive_offer_test.go @@ -125,7 +125,7 @@ func TestCreatePassiveSellOfferPrice(t *testing.T) { SourceAccount: kp0.Address(), } - xdrOp, err := offer.BuildXDR() + xdrOp, err := offer.BuildXDR(false) assert.NoError(t, err) expectedPrice := xdr.Price{N: 1, D: 1000000000} assert.Equal(t, expectedPrice, xdrOp.Body.CreatePassiveSellOfferOp.Price) @@ -133,7 +133,7 @@ func TestCreatePassiveSellOfferPrice(t *testing.T) { assert.Equal(t, expectedPrice, offer.price.toXDR()) parsed := CreatePassiveSellOffer{} - assert.NoError(t, parsed.FromXDR(xdrOp)) + assert.NoError(t, parsed.FromXDR(xdrOp, false)) assert.Equal(t, offer.Price, parsed.Price) assert.Equal(t, offer.price, parsed.price) } diff --git a/txnbuild/end_sponsoring_future_reserves.go b/txnbuild/end_sponsoring_future_reserves.go index 297d4ceee8..e0617a1891 100644 --- a/txnbuild/end_sponsoring_future_reserves.go +++ b/txnbuild/end_sponsoring_future_reserves.go @@ -14,30 +14,34 @@ type EndSponsoringFutureReserves struct { } // BuildXDR for EndSponsoringFutureReserves returns a fully configured XDR Operation. -func (es *EndSponsoringFutureReserves) BuildXDR() (xdr.Operation, error) { +func (es *EndSponsoringFutureReserves) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { opType := xdr.OperationTypeEndSponsoringFutureReserves body, err := xdr.NewOperationBody(opType, nil) if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, es.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, es.SourceAccount) + } else { + SetOpSourceAccount(&op, es.SourceAccount) + } return op, nil } // FromXDR for EndSponsoringFutureReserves initializes the txnbuild struct from the corresponding xdr Operation. -func (es *EndSponsoringFutureReserves) FromXDR(xdrOp xdr.Operation) error { +func (es *EndSponsoringFutureReserves) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { if xdrOp.Body.Type != xdr.OperationTypeEndSponsoringFutureReserves { return errors.New("error parsing end_sponsoring_future_reserves operation from xdr") } - es.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + es.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) return nil } // Validate for EndSponsoringFutureReserves validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (es *EndSponsoringFutureReserves) Validate() error { +func (es *EndSponsoringFutureReserves) Validate(withMuxedAccounts bool) error { return nil } diff --git a/txnbuild/end_sponsoring_future_reserves_test.go b/txnbuild/end_sponsoring_future_reserves_test.go index e11601f472..8b5ab5cdde 100644 --- a/txnbuild/end_sponsoring_future_reserves_test.go +++ b/txnbuild/end_sponsoring_future_reserves_test.go @@ -3,5 +3,8 @@ package txnbuild import "testing" func TestEndSponsoringFutureReservesRoundTrip(t *testing.T) { - testOperationsMarshallingRoundtrip(t, []Operation{&EndSponsoringFutureReserves{}}) + withoutMuxedAccounts := &EndSponsoringFutureReserves{SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"} + testOperationsMarshallingRoundtrip(t, []Operation{withoutMuxedAccounts}, false) + withMuxedAccounts := &EndSponsoringFutureReserves{SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"} + testOperationsMarshallingRoundtrip(t, []Operation{withMuxedAccounts}, true) } diff --git a/txnbuild/fee_bump_test.go b/txnbuild/fee_bump_test.go index b731dfa744..63162b96fb 100644 --- a/txnbuild/fee_bump_test.go +++ b/txnbuild/fee_bump_test.go @@ -346,92 +346,59 @@ func TestFeeBumpAddSignatureBase64(t *testing.T) { assert.Equal(t, expected, b64) } -func TestFeeBumpRoundTrip(t *testing.T) { +func TestFeeBumpMuxedAccounts(t *testing.T) { kp0, kp1 := newKeypair0(), newKeypair1() - sourceAccount := NewSimpleAccount(kp0.Address(), 1) - + accountID0 := xdr.MustAddress(kp0.Address()) + mx0 := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *accountID0.Ed25519, + }, + } + sourceAccount := NewSimpleAccount(mx0.Address(), 1) tx, err := NewTransaction( TransactionParams{ - SourceAccount: &sourceAccount, - Operations: []Operation{&Inflation{}}, - BaseFee: MinBaseFee, - Timebounds: NewInfiniteTimeout(), + SourceAccount: &sourceAccount, + Operations: []Operation{&Inflation{}}, + BaseFee: MinBaseFee, + Timebounds: NewInfiniteTimeout(), + EnableMuxedAccounts: true, }, ) assert.NoError(t, err) tx, err = tx.Sign(network.TestNetworkPassphrase, kp0) assert.NoError(t, err) - expectedInnerB64, err := tx.Base64() - assert.NoError(t, err) + accountID1 := xdr.MustAddress(kp1.Address()) + mx1 := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xdeadbeef, + Ed25519: *accountID1.Ed25519, + }, + } feeBumpTx, err := NewFeeBumpTransaction( FeeBumpTransactionParams{ - FeeAccount: kp1.Address(), - BaseFee: 2 * MinBaseFee, - Inner: tx, + FeeAccount: mx1.Address(), + BaseFee: 2 * MinBaseFee, + Inner: tx, + EnableMuxedAccounts: true, }, ) assert.NoError(t, err) - feeBumpTx, err = feeBumpTx.Sign(network.TestNetworkPassphrase, kp1) - assert.NoError(t, err) - - innerB64, err := feeBumpTx.InnerTransaction().Base64() - assert.NoError(t, err) - assert.Equal(t, expectedInnerB64, innerB64) + assert.Equal(t, mx0.Address(), feeBumpTx.InnerTransaction().sourceAccount.AccountID) + assert.Equal(t, mx1.Address(), feeBumpTx.FeeAccount()) - assert.Equal(t, kp1.Address(), feeBumpTx.FeeAccount()) - assert.Equal(t, int64(2*MinBaseFee), feeBumpTx.BaseFee()) - assert.Equal(t, int64(4*MinBaseFee), feeBumpTx.MaxFee()) - - outerHash, err := feeBumpTx.HashHex(network.TestNetworkPassphrase) - assert.NoError(t, err) - - env := feeBumpTx.ToXDR() - assert.NoError(t, err) - assert.Equal(t, xdr.EnvelopeTypeEnvelopeTypeTxFeeBump, env.Type) - assert.Equal(t, xdr.MustAddress(kp1.Address()), env.FeeBumpAccount().ToAccountId()) - assert.Equal(t, int64(4*MinBaseFee), env.FeeBumpFee()) - assert.Equal(t, feeBumpTx.Signatures(), env.FeeBumpSignatures()) - innerB64, err = xdr.MarshalBase64(xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: env.FeeBump.Tx.InnerTx.V1, - }) - assert.NoError(t, err) - assert.Equal(t, expectedInnerB64, innerB64) - - expectedFeeBumpB64, err := xdr.MarshalBase64(env) - assert.NoError(t, err) - - b64, err := feeBumpTx.Base64() - assert.NoError(t, err) - assert.Equal(t, expectedFeeBumpB64, b64) - - binary, err := feeBumpTx.MarshalBinary() - assert.NoError(t, err) - assert.Equal(t, expectedFeeBumpB64, base64.StdEncoding.EncodeToString(binary)) - - parsed, err := TransactionFromXDR(expectedFeeBumpB64) - assert.NoError(t, err) - parsedFeeBump, ok := parsed.FeeBump() - assert.True(t, ok) - _, ok = parsed.Transaction() - assert.False(t, ok) - - parsedHash, err := parsedFeeBump.HashHex(network.TestNetworkPassphrase) - assert.NoError(t, err) - - assert.Equal(t, feeBumpTx.Signatures(), parsedFeeBump.Signatures()) - assert.Equal(t, kp1.Address(), parsedFeeBump.FeeAccount()) - assert.Equal(t, int64(2*MinBaseFee), parsedFeeBump.BaseFee()) - assert.Equal(t, int64(4*MinBaseFee), parsedFeeBump.MaxFee()) - innerB64, err = xdr.MarshalBase64(xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: parsedFeeBump.envelope.FeeBump.Tx.InnerTx.V1, - }) - assert.NoError(t, err) - assert.Equal(t, expectedInnerB64, innerB64) - b64, err = parsedFeeBump.Base64() - assert.NoError(t, err) - assert.Equal(t, expectedFeeBumpB64, b64) - assert.Equal(t, outerHash, parsedHash) + // It fails when not enabling muxed accounts + feeBumpTx, err = NewFeeBumpTransaction( + FeeBumpTransactionParams{ + FeeAccount: mx1.Address(), + BaseFee: 2 * MinBaseFee, + Inner: tx, + EnableMuxedAccounts: false, + }, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid version byte") } diff --git a/txnbuild/inflation.go b/txnbuild/inflation.go index 1944b9c2e6..0677bed2fc 100644 --- a/txnbuild/inflation.go +++ b/txnbuild/inflation.go @@ -12,29 +12,33 @@ type Inflation struct { } // BuildXDR for Inflation returns a fully configured XDR Operation. -func (inf *Inflation) BuildXDR() (xdr.Operation, error) { +func (inf *Inflation) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { opType := xdr.OperationTypeInflation body, err := xdr.NewOperationBody(opType, nil) if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, inf.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, inf.SourceAccount) + } else { + SetOpSourceAccount(&op, inf.SourceAccount) + } return op, nil } // FromXDR for Inflation initialises the txnbuild struct from the corresponding xdr Operation. -func (inf *Inflation) FromXDR(xdrOp xdr.Operation) error { +func (inf *Inflation) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { if xdrOp.Body.Type != xdr.OperationTypeInflation { return errors.New("error parsing inflation operation from xdr") } - inf.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + inf.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) return nil } // Validate for Inflation is just a method that implements the Operation interface. No logic is actually performed // because the inflation operation does not have any required field. Nil is always returned. -func (inf *Inflation) Validate() error { +func (inf *Inflation) Validate(withMuxedAccounts bool) error { // no required fields, return nil. return nil } diff --git a/txnbuild/inflation_test.go b/txnbuild/inflation_test.go new file mode 100644 index 0000000000..038dc10d08 --- /dev/null +++ b/txnbuild/inflation_test.go @@ -0,0 +1,16 @@ +package txnbuild + +import "testing" + +func TestInflationRoundtrip(t *testing.T) { + inflation := Inflation{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + } + testOperationsMarshallingRoundtrip(t, []Operation{&inflation}, false) + + // with muxed accounts + inflation = Inflation{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + } + testOperationsMarshallingRoundtrip(t, []Operation{&inflation}, true) +} diff --git a/txnbuild/manage_buy_offer.go b/txnbuild/manage_buy_offer.go index 030e750dd0..e6badcb19e 100644 --- a/txnbuild/manage_buy_offer.go +++ b/txnbuild/manage_buy_offer.go @@ -19,7 +19,7 @@ type ManageBuyOffer struct { } // BuildXDR for ManageBuyOffer returns a fully configured XDR Operation. -func (mo *ManageBuyOffer) BuildXDR() (xdr.Operation, error) { +func (mo *ManageBuyOffer) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrSelling, err := mo.Selling.ToXDR() if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set XDR 'Selling' field") @@ -53,18 +53,22 @@ func (mo *ManageBuyOffer) BuildXDR() (xdr.Operation, error) { } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, mo.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, mo.SourceAccount) + } else { + SetOpSourceAccount(&op, mo.SourceAccount) + } return op, nil } // FromXDR for ManageBuyOffer initialises the txnbuild struct from the corresponding xdr Operation. -func (mo *ManageBuyOffer) FromXDR(xdrOp xdr.Operation) error { +func (mo *ManageBuyOffer) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetManageBuyOfferOp() if !ok { return errors.New("error parsing manage_buy_offer operation from xdr") } - mo.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + mo.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) mo.OfferID = int64(result.OfferId) mo.Amount = amount.String(result.BuyAmount) if result.Price != (xdr.Price{}) { @@ -87,7 +91,7 @@ func (mo *ManageBuyOffer) FromXDR(xdrOp xdr.Operation) error { // Validate for ManageBuyOffer validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (mo *ManageBuyOffer) Validate() error { +func (mo *ManageBuyOffer) Validate(withMuxedAccounts bool) error { return validateOffer(mo.Buying, mo.Selling, mo.Amount, mo.Price, mo.OfferID) } diff --git a/txnbuild/manage_buy_offer_test.go b/txnbuild/manage_buy_offer_test.go index 3eecbb49fc..43e5230612 100644 --- a/txnbuild/manage_buy_offer_test.go +++ b/txnbuild/manage_buy_offer_test.go @@ -158,7 +158,7 @@ func TestManageBuyOfferPrice(t *testing.T) { OfferID: 1, } - xdrOp, err := mbo.BuildXDR() + xdrOp, err := mbo.BuildXDR(false) assert.NoError(t, err) expectedPrice := xdr.Price{N: 1, D: 1000000000} assert.Equal(t, expectedPrice, xdrOp.Body.ManageBuyOfferOp.Price) @@ -166,7 +166,30 @@ func TestManageBuyOfferPrice(t *testing.T) { assert.Equal(t, expectedPrice, mbo.price.toXDR()) parsed := ManageBuyOffer{} - assert.NoError(t, parsed.FromXDR(xdrOp)) + assert.NoError(t, parsed.FromXDR(xdrOp, false)) assert.Equal(t, mbo.Price, parsed.Price) assert.Equal(t, mbo.price, parsed.price) } + +func TestManageBuyOfferRoundtrip(t *testing.T) { + manageBuyOffer := ManageBuyOffer{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Selling: CreditAsset{"USD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + Buying: NativeAsset{}, + Amount: "100.0000000", + Price: "0.01", + OfferID: 0, + } + testOperationsMarshallingRoundtrip(t, []Operation{&manageBuyOffer}, false) + + // with muxed accounts + manageBuyOffer = ManageBuyOffer{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Selling: CreditAsset{"USD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + Buying: NativeAsset{}, + Amount: "100.0000000", + Price: "0.01", + OfferID: 0, + } + testOperationsMarshallingRoundtrip(t, []Operation{&manageBuyOffer}, true) +} diff --git a/txnbuild/manage_data.go b/txnbuild/manage_data.go index 911d01ed0f..38194b05e6 100644 --- a/txnbuild/manage_data.go +++ b/txnbuild/manage_data.go @@ -14,7 +14,7 @@ type ManageData struct { } // BuildXDR for ManageData returns a fully configured XDR Operation. -func (md *ManageData) BuildXDR() (xdr.Operation, error) { +func (md *ManageData) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrOp := xdr.ManageDataOp{DataName: xdr.String64(md.Name)} // No data value clears the named data entry on the account @@ -31,18 +31,22 @@ func (md *ManageData) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, md.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, md.SourceAccount) + } else { + SetOpSourceAccount(&op, md.SourceAccount) + } return op, nil } // FromXDR for ManageData initialises the txnbuild struct from the corresponding xdr Operation. -func (md *ManageData) FromXDR(xdrOp xdr.Operation) error { +func (md *ManageData) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetManageDataOp() if !ok { return errors.New("error parsing create_account operation from xdr") } - md.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + md.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) md.Name = string(result.DataName) if result.DataValue != nil { md.Value = *result.DataValue @@ -54,7 +58,7 @@ func (md *ManageData) FromXDR(xdrOp xdr.Operation) error { // Validate for ManageData validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (md *ManageData) Validate() error { +func (md *ManageData) Validate(withMuxedAccounts bool) error { if len(md.Name) > 64 { return NewValidationError("Name", "maximum length is 64 characters") } diff --git a/txnbuild/manage_data_test.go b/txnbuild/manage_data_test.go index a4303e0afe..9c8074371a 100644 --- a/txnbuild/manage_data_test.go +++ b/txnbuild/manage_data_test.go @@ -126,3 +126,20 @@ func TestManageDataRoundTrip(t *testing.T) { }) } } + +func TestManageDataRoundtrip(t *testing.T) { + manageData := ManageData{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Name: "foo", + Value: []byte("bar"), + } + testOperationsMarshallingRoundtrip(t, []Operation{&manageData}, false) + + // with muxed accounts + manageData = ManageData{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Name: "foo", + Value: []byte("bar"), + } + testOperationsMarshallingRoundtrip(t, []Operation{&manageData}, true) +} diff --git a/txnbuild/manage_offer.go b/txnbuild/manage_offer.go index 021250aedb..248ffc2ada 100644 --- a/txnbuild/manage_offer.go +++ b/txnbuild/manage_offer.go @@ -6,7 +6,7 @@ import ( "github.com/stellar/go/xdr" ) -//CreateOfferOp returns a ManageSellOffer operation to create a new offer, by +// CreateOfferOp returns a ManageSellOffer operation to create a new offer, by // setting the OfferID to "0". The sourceAccount is optional, and if not provided, // will be that of the surrounding transaction. func CreateOfferOp(selling, buying Asset, amount, price string, sourceAccount ...string) (ManageSellOffer, error) { @@ -46,7 +46,7 @@ func UpdateOfferOp(selling, buying Asset, amount, price string, offerID int64, s return offer, nil } -//DeleteOfferOp returns a ManageSellOffer operation to delete an offer, by +// DeleteOfferOp returns a ManageSellOffer operation to delete an offer, by // setting the Amount to "0". The sourceAccount is optional, and if not provided, // will be that of the surrounding transaction. func DeleteOfferOp(offerID int64, sourceAccount ...string) (ManageSellOffer, error) { @@ -83,7 +83,7 @@ type ManageSellOffer struct { } // BuildXDR for ManageSellOffer returns a fully configured XDR Operation. -func (mo *ManageSellOffer) BuildXDR() (xdr.Operation, error) { +func (mo *ManageSellOffer) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrSelling, err := mo.Selling.ToXDR() if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set XDR 'Selling' field") @@ -117,18 +117,22 @@ func (mo *ManageSellOffer) BuildXDR() (xdr.Operation, error) { } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, mo.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, mo.SourceAccount) + } else { + SetOpSourceAccount(&op, mo.SourceAccount) + } return op, nil } // FromXDR for ManageSellOffer initialises the txnbuild struct from the corresponding xdr Operation. -func (mo *ManageSellOffer) FromXDR(xdrOp xdr.Operation) error { +func (mo *ManageSellOffer) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetManageSellOfferOp() if !ok { return errors.New("error parsing manage_sell_offer operation from xdr") } - mo.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + mo.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) mo.OfferID = int64(result.OfferId) mo.Amount = amount.String(result.Amount) if result.Price != (xdr.Price{}) { @@ -151,7 +155,7 @@ func (mo *ManageSellOffer) FromXDR(xdrOp xdr.Operation) error { // Validate for ManageSellOffer validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (mo *ManageSellOffer) Validate() error { +func (mo *ManageSellOffer) Validate(withMuxedAccounts bool) error { return validateOffer(mo.Buying, mo.Selling, mo.Amount, mo.Price, mo.OfferID) } diff --git a/txnbuild/manage_offer_test.go b/txnbuild/manage_offer_test.go index feee18c4b7..87a978368c 100644 --- a/txnbuild/manage_offer_test.go +++ b/txnbuild/manage_offer_test.go @@ -154,7 +154,7 @@ func TestManageSellOfferPrice(t *testing.T) { OfferID: 1, } - xdrOp, err := mso.BuildXDR() + xdrOp, err := mso.BuildXDR(false) assert.NoError(t, err) expectedPrice := xdr.Price{N: 1, D: 1000000000} assert.Equal(t, expectedPrice, xdrOp.Body.ManageSellOfferOp.Price) @@ -162,7 +162,30 @@ func TestManageSellOfferPrice(t *testing.T) { assert.Equal(t, expectedPrice, mso.price.toXDR()) parsed := ManageSellOffer{} - assert.NoError(t, parsed.FromXDR(xdrOp)) + assert.NoError(t, parsed.FromXDR(xdrOp, false)) assert.Equal(t, mso.Price, parsed.Price) assert.Equal(t, mso.price, parsed.price) } + +func TestManageSellOfferRoundtrip(t *testing.T) { + manageSellOffer := ManageSellOffer{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Selling: CreditAsset{"USD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + Buying: NativeAsset{}, + Amount: "100.0000000", + Price: "0.01", + OfferID: 0, + } + testOperationsMarshallingRoundtrip(t, []Operation{&manageSellOffer}, false) + + // with muxed accounts + manageSellOffer = ManageSellOffer{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Selling: CreditAsset{"USD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + Buying: NativeAsset{}, + Amount: "100.0000000", + Price: "0.01", + OfferID: 0, + } + testOperationsMarshallingRoundtrip(t, []Operation{&manageSellOffer}, true) +} diff --git a/txnbuild/operation.go b/txnbuild/operation.go index 46a2f92adb..58a3298c9c 100644 --- a/txnbuild/operation.go +++ b/txnbuild/operation.go @@ -8,14 +8,24 @@ import ( // Operation represents the operation types of the Stellar network. type Operation interface { - BuildXDR() (xdr.Operation, error) - FromXDR(xdrOp xdr.Operation) error - Validate() error + BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) + FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error + Validate(withMuxedAccounts bool) error GetSourceAccount() string } // SetOpSourceAccount sets the source account ID on an Operation. func SetOpSourceAccount(op *xdr.Operation, sourceAccount string) { + if sourceAccount == "" { + return + } + var opSourceAccountID xdr.MuxedAccount + opSourceAccountID.SetEd25519Address(sourceAccount) + op.SourceAccount = &opSourceAccountID +} + +// SetOpSourceAccount sets the source account ID on an Operation, allowing M-strkeys (as defined in SEP23). +func SetOpSourceMuxedAccount(op *xdr.Operation, sourceAccount string) { if sourceAccount == "" { return } @@ -25,7 +35,7 @@ func SetOpSourceAccount(op *xdr.Operation, sourceAccount string) { } // operationFromXDR returns a txnbuild Operation from its corresponding XDR operation -func operationFromXDR(xdrOp xdr.Operation) (Operation, error) { +func operationFromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) (Operation, error) { var newOp Operation switch xdrOp.Body.Type { case xdr.OperationTypeCreateAccount: @@ -76,14 +86,18 @@ func operationFromXDR(xdrOp xdr.Operation) (Operation, error) { return nil, fmt.Errorf("unknown operation type: %d", xdrOp.Body.Type) } - err := newOp.FromXDR(xdrOp) + err := newOp.FromXDR(xdrOp, withMuxedAccounts) return newOp, err } -func accountFromXDR(account *xdr.MuxedAccount) string { +func accountFromXDR(account *xdr.MuxedAccount, withMuxedAccounts bool) string { if account != nil { - aid := account.ToAccountId() - return aid.Address() + if withMuxedAccounts { + return account.Address() + } else { + aid := account.ToAccountId() + return aid.Address() + } } return "" } diff --git a/txnbuild/operation_test.go b/txnbuild/operation_test.go index f808bb3420..f8de64842d 100644 --- a/txnbuild/operation_test.go +++ b/txnbuild/operation_test.go @@ -15,7 +15,7 @@ func TestCreateAccountFromXDR(t *testing.T) { xdrEnv, err := unmarshalBase64(txeB64) if assert.NoError(t, err) { var ca CreateAccount - err = ca.FromXDR(xdrEnv.Operations()[0]) + err = ca.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", ca.SourceAccount, "source accounts should match") assert.Equal(t, "GCPHE7DYMAUAY4UWA6OIYDFGLKRXGLLEMT6MVETC36L7LW4Z3A37EJW5", ca.Destination, "destination should match") @@ -27,7 +27,7 @@ func TestCreateAccountFromXDR(t *testing.T) { xdrEnv, err = unmarshalBase64(txeB64NoSource) if assert.NoError(t, err) { var ca CreateAccount - err = ca.FromXDR(xdrEnv.Operations()[0]) + err = ca.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "", ca.SourceAccount, "source accounts should match") assert.Equal(t, "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", ca.Destination, "destination should match") @@ -69,7 +69,7 @@ func TestPaymentFromXDR(t *testing.T) { xdrEnv, err := unmarshalBase64(txeB64) if assert.NoError(t, err) { var p Payment - err = p.FromXDR(xdrEnv.Operations()[0]) + err = p.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "GBUKBCG5VLRKAVYAIREJRUJHOKLIADZJOICRW43WVJCLES52BDOTCQZU", p.SourceAccount, "source accounts should match") assert.Equal(t, "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", p.Destination, "destination should match") @@ -77,7 +77,7 @@ func TestPaymentFromXDR(t *testing.T) { assert.Equal(t, true, p.Asset.IsNative(), "Asset should be native") } - err = p.FromXDR(xdrEnv.Operations()[1]) + err = p.FromXDR(xdrEnv.Operations()[1], false) if assert.NoError(t, err) { assert.Equal(t, "", p.SourceAccount, "source accounts should match") assert.Equal(t, "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", p.Destination, "destination should match") @@ -97,7 +97,7 @@ func TestPathPaymentFromXDR(t *testing.T) { xdrEnv, err := unmarshalBase64(txeB64) if assert.NoError(t, err) { var pp PathPayment - err = pp.FromXDR(xdrEnv.Operations()[0]) + err = pp.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "", pp.SourceAccount, "source accounts should match") assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", pp.Destination, "destination should match") @@ -120,7 +120,7 @@ func TestManageSellOfferFromXDR(t *testing.T) { xdrEnv, err := unmarshalBase64(txeB64) if assert.NoError(t, err) { var mso ManageSellOffer - err = mso.FromXDR(xdrEnv.Operations()[0]) + err = mso.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "GBUKBCG5VLRKAVYAIREJRUJHOKLIADZJOICRW43WVJCLES52BDOTCQZU", mso.SourceAccount, "source accounts should match") assert.Equal(t, int64(0), mso.OfferID, "OfferID should match") @@ -134,7 +134,7 @@ func TestManageSellOfferFromXDR(t *testing.T) { assert.Equal(t, "GBUKBCG5VLRKAVYAIREJRUJHOKLIADZJOICRW43WVJCLES52BDOTCQZU", mso.Buying.GetIssuer(), "Asset issuer should match") } - err = mso.FromXDR(xdrEnv.Operations()[1]) + err = mso.FromXDR(xdrEnv.Operations()[1], false) if assert.NoError(t, err) { assert.Equal(t, "", mso.SourceAccount, "source accounts should match") assert.Equal(t, int64(0), mso.OfferID, "OfferID should match") @@ -157,7 +157,7 @@ func TestManageBuyOfferFromXDR(t *testing.T) { xdrEnv, err := unmarshalBase64(txeB64) if assert.NoError(t, err) { var mbo ManageBuyOffer - err = mbo.FromXDR(xdrEnv.Operations()[0]) + err = mbo.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "GBUKBCG5VLRKAVYAIREJRUJHOKLIADZJOICRW43WVJCLES52BDOTCQZU", mbo.SourceAccount, "source accounts should match") assert.Equal(t, int64(0), mbo.OfferID, "OfferID should match") @@ -171,7 +171,7 @@ func TestManageBuyOfferFromXDR(t *testing.T) { assert.Equal(t, "GBUKBCG5VLRKAVYAIREJRUJHOKLIADZJOICRW43WVJCLES52BDOTCQZU", mbo.Buying.GetIssuer(), "Asset issuer should match") } - err = mbo.FromXDR(xdrEnv.Operations()[1]) + err = mbo.FromXDR(xdrEnv.Operations()[1], false) if assert.NoError(t, err) { assert.Equal(t, "", mbo.SourceAccount, "source accounts should match") assert.Equal(t, int64(0), mbo.OfferID, "OfferID should match") @@ -194,7 +194,7 @@ func TestCreatePassiveSellOfferFromXDR(t *testing.T) { xdrEnv, err := unmarshalBase64(txeB64) if assert.NoError(t, err) { var cpo CreatePassiveSellOffer - err = cpo.FromXDR(xdrEnv.Operations()[0]) + err = cpo.FromXDR(xdrEnv.Operations()[0], false) if assert.NoError(t, err) { assert.Equal(t, "", cpo.SourceAccount, "source accounts should match") assert.Equal(t, "10.0000000", cpo.Amount, "Amount should match") @@ -251,7 +251,7 @@ func TestSetOptionsFromXDR(t *testing.T) { } var so SetOptions - err = so.FromXDR(xdrOp) + err = so.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", so.SourceAccount, "source accounts should match") assert.Equal(t, Threshold(7), *so.MasterWeight, "master weight should match") @@ -284,7 +284,7 @@ func TestChangeTrustFromXDR(t *testing.T) { } var opSource xdr.MuxedAccount - err = opSource.SetAddress("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") + err = opSource.SetEd25519Address("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") assert.NoError(t, err) xdrOp := xdr.Operation{ SourceAccount: &opSource, @@ -295,7 +295,7 @@ func TestChangeTrustFromXDR(t *testing.T) { } var ct ChangeTrust - err = ct.FromXDR(xdrOp) + err = ct.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", ct.SourceAccount, "source accounts should match") assetType, e := ct.Line.GetType() @@ -314,7 +314,7 @@ func TestAllowTrustFromXDR(t *testing.T) { assert.NoError(t, err) var opSource xdr.MuxedAccount - err = opSource.SetAddress("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") + err = opSource.SetEd25519Address("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") assert.NoError(t, err) var trustor xdr.AccountId @@ -336,7 +336,7 @@ func TestAllowTrustFromXDR(t *testing.T) { } var at AllowTrust - err = at.FromXDR(xdrOp) + err = at.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", at.SourceAccount, "source accounts should match") @@ -351,11 +351,11 @@ func TestAllowTrustFromXDR(t *testing.T) { func TestAccountMergeFromXDR(t *testing.T) { var opSource xdr.MuxedAccount - err := opSource.SetAddress("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") + err := opSource.SetEd25519Address("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") assert.NoError(t, err) var destination xdr.MuxedAccount - err = destination.SetAddress("GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3") + err = destination.SetEd25519Address("GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3") assert.NoError(t, err) xdrOp := xdr.Operation{ @@ -367,7 +367,7 @@ func TestAccountMergeFromXDR(t *testing.T) { } var am AccountMerge - err = am.FromXDR(xdrOp) + err = am.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", am.SourceAccount, "source accounts should match") assert.Equal(t, "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", am.Destination, "destination accounts should match") @@ -376,7 +376,7 @@ func TestAccountMergeFromXDR(t *testing.T) { func TestInflationFromXDR(t *testing.T) { var opSource xdr.MuxedAccount - err := opSource.SetAddress("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") + err := opSource.SetEd25519Address("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") assert.NoError(t, err) xdrOp := xdr.Operation{ @@ -385,7 +385,7 @@ func TestInflationFromXDR(t *testing.T) { } var inf Inflation - err = inf.FromXDR(xdrOp) + err = inf.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", inf.SourceAccount, "source accounts should match") } @@ -393,7 +393,7 @@ func TestInflationFromXDR(t *testing.T) { func TestManageDataFromXDR(t *testing.T) { var opSource xdr.MuxedAccount - err := opSource.SetAddress("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") + err := opSource.SetEd25519Address("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") assert.NoError(t, err) dv := []byte("value") @@ -412,7 +412,7 @@ func TestManageDataFromXDR(t *testing.T) { } var md ManageData - err = md.FromXDR(xdrOp) + err = md.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", md.SourceAccount, "source accounts should match") assert.Equal(t, "data", md.Name, "Name should match") @@ -422,7 +422,7 @@ func TestManageDataFromXDR(t *testing.T) { func TestBumpSequenceFromXDR(t *testing.T) { var opSource xdr.MuxedAccount - err := opSource.SetAddress("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") + err := opSource.SetEd25519Address("GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H") assert.NoError(t, err) bsOp := xdr.BumpSequenceOp{ @@ -438,23 +438,37 @@ func TestBumpSequenceFromXDR(t *testing.T) { } var bs BumpSequence - err = bs.FromXDR(xdrOp) + err = bs.FromXDR(xdrOp, false) if assert.NoError(t, err) { assert.Equal(t, "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", bs.SourceAccount, "source accounts should match") assert.Equal(t, int64(45), bs.BumpTo, "BumpTo should match") } } -func testOperationsMarshallingRoundtrip(t *testing.T, operations []Operation) { +func testOperationsMarshallingRoundtrip(t *testing.T, operations []Operation, withMuxedAccounts bool) { kp1 := newKeypair1() - sourceAccount := NewSimpleAccount(kp1.Address(), int64(9606132444168199)) + accountID := xdr.MustAddress(kp1.Address()) + mx := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *accountID.Ed25519, + }, + } + var sourceAccount SimpleAccount + if withMuxedAccounts { + sourceAccount = NewSimpleAccount(mx.Address(), int64(9605939170639898)) + } else { + sourceAccount = NewSimpleAccount(kp1.Address(), int64(9606132444168199)) + } tx, err := NewTransaction( TransactionParams{ - SourceAccount: &sourceAccount, - Operations: operations, - Timebounds: NewInfiniteTimeout(), - BaseFee: MinBaseFee, + SourceAccount: &sourceAccount, + Operations: operations, + Timebounds: NewInfiniteTimeout(), + BaseFee: MinBaseFee, + EnableMuxedAccounts: withMuxedAccounts, }, ) assert.NoError(t, err) @@ -464,7 +478,11 @@ func testOperationsMarshallingRoundtrip(t *testing.T, operations []Operation) { assert.NoError(t, err) var parsedTx *GenericTransaction - parsedTx, err = TransactionFromXDR(b64) + if withMuxedAccounts { + parsedTx, err = TransactionFromXDR(b64, TransactionFromXDROptionEnableMuxedAccounts) + } else { + parsedTx, err = TransactionFromXDR(b64) + } assert.NoError(t, err) var ok bool tx, ok = parsedTx.Transaction() diff --git a/txnbuild/path_payment.go b/txnbuild/path_payment.go index c60c2c4370..26cb9967ea 100644 --- a/txnbuild/path_payment.go +++ b/txnbuild/path_payment.go @@ -25,7 +25,7 @@ type PathPaymentStrictReceive struct { } // BuildXDR for PathPaymentStrictReceive returns a fully configured XDR Operation. -func (pp *PathPaymentStrictReceive) BuildXDR() (xdr.Operation, error) { +func (pp *PathPaymentStrictReceive) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { // Set XDR send asset if pp.SendAsset == nil { return xdr.Operation{}, errors.New("you must specify an asset to send for payment") @@ -43,7 +43,11 @@ func (pp *PathPaymentStrictReceive) BuildXDR() (xdr.Operation, error) { // Set XDR destination var xdrDestination xdr.MuxedAccount - err = xdrDestination.SetAddress(pp.Destination) + if withMuxedAccounts { + err = xdrDestination.SetAddress(pp.Destination) + } else { + err = xdrDestination.SetEd25519Address(pp.Destination) + } if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set destination address") } @@ -88,20 +92,28 @@ func (pp *PathPaymentStrictReceive) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, pp.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, pp.SourceAccount) + } else { + SetOpSourceAccount(&op, pp.SourceAccount) + } return op, nil } // FromXDR for PathPaymentStrictReceive initialises the txnbuild struct from the corresponding xdr Operation. -func (pp *PathPaymentStrictReceive) FromXDR(xdrOp xdr.Operation) error { +func (pp *PathPaymentStrictReceive) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetPathPaymentStrictReceiveOp() if !ok { return errors.New("error parsing path_payment operation from xdr") } - pp.SourceAccount = accountFromXDR(xdrOp.SourceAccount) - destAID := result.Destination.ToAccountId() - pp.Destination = destAID.Address() + pp.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) + if withMuxedAccounts { + pp.Destination = result.Destination.Address() + } else { + destAID := result.Destination.ToAccountId() + pp.Destination = destAID.Address() + } pp.DestAmount = amount.String(result.DestAmount) pp.SendMax = amount.String(result.SendMax) @@ -131,8 +143,14 @@ func (pp *PathPaymentStrictReceive) FromXDR(xdrOp xdr.Operation) error { // Validate for PathPaymentStrictReceive validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (pp *PathPaymentStrictReceive) Validate() error { - _, err := xdr.AddressToAccountId(pp.Destination) +func (pp *PathPaymentStrictReceive) Validate(withMuxedAccounts bool) error { + var err error + if withMuxedAccounts { + _, err = xdr.AddressToMuxedAccount(pp.Destination) + } else { + _, err = xdr.AddressToAccountId(pp.Destination) + } + if err != nil { return NewValidationError("Destination", err.Error()) } diff --git a/txnbuild/path_payment_strict_send.go b/txnbuild/path_payment_strict_send.go index 07eb720c7c..2b9726a32d 100644 --- a/txnbuild/path_payment_strict_send.go +++ b/txnbuild/path_payment_strict_send.go @@ -19,7 +19,7 @@ type PathPaymentStrictSend struct { } // BuildXDR for Payment returns a fully configured XDR Operation. -func (pp *PathPaymentStrictSend) BuildXDR() (xdr.Operation, error) { +func (pp *PathPaymentStrictSend) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { // Set XDR send asset if pp.SendAsset == nil { return xdr.Operation{}, errors.New("you must specify an asset to send for payment") @@ -37,7 +37,11 @@ func (pp *PathPaymentStrictSend) BuildXDR() (xdr.Operation, error) { // Set XDR destination var xdrDestination xdr.MuxedAccount - err = xdrDestination.SetAddress(pp.Destination) + if withMuxedAccounts { + err = xdrDestination.SetAddress(pp.Destination) + } else { + err = xdrDestination.SetEd25519Address(pp.Destination) + } if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set destination address") } @@ -82,20 +86,28 @@ func (pp *PathPaymentStrictSend) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, pp.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, pp.SourceAccount) + } else { + SetOpSourceAccount(&op, pp.SourceAccount) + } return op, nil } // FromXDR for PathPaymentStrictSend initialises the txnbuild struct from the corresponding xdr Operation. -func (pp *PathPaymentStrictSend) FromXDR(xdrOp xdr.Operation) error { +func (pp *PathPaymentStrictSend) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetPathPaymentStrictSendOp() if !ok { return errors.New("error parsing path_payment operation from xdr") } - pp.SourceAccount = accountFromXDR(xdrOp.SourceAccount) - destAID := result.Destination.ToAccountId() - pp.Destination = destAID.Address() + pp.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) + if withMuxedAccounts { + pp.Destination = result.Destination.Address() + } else { + destAID := result.Destination.ToAccountId() + pp.Destination = destAID.Address() + } pp.SendAmount = amount.String(result.SendAmount) pp.DestMin = amount.String(result.DestMin) @@ -125,8 +137,13 @@ func (pp *PathPaymentStrictSend) FromXDR(xdrOp xdr.Operation) error { // Validate for PathPaymentStrictSend validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (pp *PathPaymentStrictSend) Validate() error { - _, err := xdr.AddressToAccountId(pp.Destination) +func (pp *PathPaymentStrictSend) Validate(withMuxedAccounts bool) error { + var err error + if withMuxedAccounts { + _, err = xdr.AddressToMuxedAccount(pp.Destination) + } else { + _, err = xdr.AddressToAccountId(pp.Destination) + } if err != nil { return NewValidationError("Destination", err.Error()) } diff --git a/txnbuild/path_payment_strict_send_test.go b/txnbuild/path_payment_strict_send_test.go index 66a79a7b58..3e3d73a01a 100644 --- a/txnbuild/path_payment_strict_send_test.go +++ b/txnbuild/path_payment_strict_send_test.go @@ -156,3 +156,28 @@ func TestPathPaymentStrictSendValidateDestAmount(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestPathPaymentStrictSendRoundtrip(t *testing.T) { + pathPaymentStrictSend := PathPaymentStrictSend{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + SendAsset: NativeAsset{}, + SendAmount: "10.0000000", + Destination: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + DestAsset: CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + DestMin: "1.0000000", + Path: []Asset{CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&pathPaymentStrictSend}, false) + + // with muxed accounts + pathPaymentStrictSend = PathPaymentStrictSend{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + SendAsset: NativeAsset{}, + SendAmount: "10.0000000", + Destination: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + DestAsset: CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + DestMin: "1.0000000", + Path: []Asset{CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&pathPaymentStrictSend}, true) +} diff --git a/txnbuild/path_payment_test.go b/txnbuild/path_payment_test.go index e8af782046..f5f78fafa7 100644 --- a/txnbuild/path_payment_test.go +++ b/txnbuild/path_payment_test.go @@ -156,3 +156,28 @@ func TestPathPaymentValidateDestAmount(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestPathPaymentRoundtrip(t *testing.T) { + pathPayment := PathPayment{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + SendAsset: NativeAsset{}, + SendMax: "10.0000000", + Destination: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + DestAsset: CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + DestAmount: "1.0000000", + Path: []Asset{CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&pathPayment}, false) + + // with muxed accounts + pathPayment = PathPayment{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + SendAsset: NativeAsset{}, + SendMax: "10.0000000", + Destination: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + DestAsset: CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}, + DestAmount: "1.0000000", + Path: []Asset{CreditAsset{"ABCD", "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H"}}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&pathPayment}, true) +} diff --git a/txnbuild/payment.go b/txnbuild/payment.go index 22356872d6..c183cfe144 100644 --- a/txnbuild/payment.go +++ b/txnbuild/payment.go @@ -16,10 +16,16 @@ type Payment struct { } // BuildXDR for Payment returns a fully configured XDR Operation. -func (p *Payment) BuildXDR() (xdr.Operation, error) { + +func (p *Payment) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var destMuxedAccount xdr.MuxedAccount - err := destMuxedAccount.SetAddress(p.Destination) + var err error + if withMuxedAccounts { + err = destMuxedAccount.SetAddress(p.Destination) + } else { + err = destMuxedAccount.SetEd25519Address(p.Destination) + } if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set destination address") } @@ -48,20 +54,29 @@ func (p *Payment) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR Operation") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, p.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, p.SourceAccount) + } else { + SetOpSourceAccount(&op, p.SourceAccount) + } return op, nil } // FromXDR for Payment initialises the txnbuild struct from the corresponding xdr Operation. -func (p *Payment) FromXDR(xdrOp xdr.Operation) error { +func (p *Payment) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetPaymentOp() if !ok { return errors.New("error parsing payment operation from xdr") } - p.SourceAccount = accountFromXDR(xdrOp.SourceAccount) - destAID := result.Destination.ToAccountId() - p.Destination = destAID.Address() + p.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) + if withMuxedAccounts { + p.Destination = result.Destination.Address() + } else { + destAID := result.Destination.ToAccountId() + p.Destination = destAID.Address() + } + p.Amount = amount.String(result.Amount) asset, err := assetFromXDR(result.Asset) @@ -75,8 +90,14 @@ func (p *Payment) FromXDR(xdrOp xdr.Operation) error { // Validate for Payment validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (p *Payment) Validate() error { - _, err := xdr.AddressToAccountId(p.Destination) +func (p *Payment) Validate(withMuxedAccounts bool) error { + var err error + if withMuxedAccounts { + _, err = xdr.AddressToMuxedAccount(p.Destination) + } else { + _, err = xdr.AddressToAccountId(p.Destination) + } + if err != nil { return NewValidationError("Destination", err.Error()) } diff --git a/txnbuild/payment_test.go b/txnbuild/payment_test.go index f8e94daa92..e82cd7b499 100644 --- a/txnbuild/payment_test.go +++ b/txnbuild/payment_test.go @@ -80,3 +80,22 @@ func TestPaymentValidateAsset(t *testing.T) { assert.Contains(t, err.Error(), expected) } } + +func TestPaymentRoundtrip(t *testing.T) { + payment := Payment{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Destination: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Amount: "10.0000000", + Asset: NativeAsset{}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&payment}, false) + + // with muxed accounts + payment = Payment{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Destination: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Amount: "10.0000000", + Asset: NativeAsset{}, + } + testOperationsMarshallingRoundtrip(t, []Operation{&payment}, true) +} diff --git a/txnbuild/revoke_sponsorship.go b/txnbuild/revoke_sponsorship.go index 293d9112fa..22e97fdadc 100644 --- a/txnbuild/revoke_sponsorship.go +++ b/txnbuild/revoke_sponsorship.go @@ -55,7 +55,7 @@ type SignerID struct { SignerAddress string } -func (r *RevokeSponsorship) BuildXDR() (xdr.Operation, error) { +func (r *RevokeSponsorship) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { xdrOp := xdr.RevokeSponsorshipOp{} switch r.SponsorshipType { case RevokeSponsorshipTypeAccount: @@ -153,12 +153,16 @@ func (r *RevokeSponsorship) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, r.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, r.SourceAccount) + } else { + SetOpSourceAccount(&op, r.SourceAccount) + } return op, nil } -func (r *RevokeSponsorship) FromXDR(xdrOp xdr.Operation) error { - r.SourceAccount = accountFromXDR(xdrOp.SourceAccount) +func (r *RevokeSponsorship) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { + r.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) op, ok := xdrOp.Body.GetRevokeSponsorshipOp() if !ok { return errors.New("error parsing revoke_sponsorhip operation from xdr") @@ -222,7 +226,7 @@ func (r *RevokeSponsorship) FromXDR(xdrOp xdr.Operation) error { return nil } -func (r *RevokeSponsorship) Validate() error { +func (r *RevokeSponsorship) Validate(withMuxedAccounts bool) error { switch r.SponsorshipType { case RevokeSponsorshipTypeAccount: if r.Account == nil { diff --git a/txnbuild/revoke_sponsorship_test.go b/txnbuild/revoke_sponsorship_test.go index 40e5687d37..46f6cb9ab3 100644 --- a/txnbuild/revoke_sponsorship_test.go +++ b/txnbuild/revoke_sponsorship_test.go @@ -88,17 +88,33 @@ func TestRevokeSponsorship(t *testing.T) { } { t.Run(testcase.name, func(t *testing.T) { op := testcase.op - assert.NoError(t, op.Validate()) - xdrOp, err := op.BuildXDR() + assert.NoError(t, op.Validate(false)) + xdrOp, err := op.BuildXDR(false) assert.NoError(t, err) xdrBin, err := xdrOp.MarshalBinary() assert.NoError(t, err) var xdrOp2 xdr.Operation assert.NoError(t, xdr.SafeUnmarshal(xdrBin, &xdrOp2)) var op2 RevokeSponsorship - assert.NoError(t, op2.FromXDR(xdrOp2)) + assert.NoError(t, op2.FromXDR(xdrOp2, false)) assert.Equal(t, op, op2) - testOperationsMarshallingRoundtrip(t, []Operation{&testcase.op}) + testOperationsMarshallingRoundtrip(t, []Operation{&testcase.op}, false) }) } + + // without muxed accounts + revokeOp := RevokeSponsorship{ + SourceAccount: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + SponsorshipType: RevokeSponsorshipTypeAccount, + Account: &accountAddress, + } + testOperationsMarshallingRoundtrip(t, []Operation{&revokeOp}, false) + + // with muxed accounts + revokeOp = RevokeSponsorship{ + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + SponsorshipType: RevokeSponsorshipTypeAccount, + Account: &accountAddress, + } + testOperationsMarshallingRoundtrip(t, []Operation{&revokeOp}, true) } diff --git a/txnbuild/set_options.go b/txnbuild/set_options.go index dcf2c9fdaa..e34a900f1e 100644 --- a/txnbuild/set_options.go +++ b/txnbuild/set_options.go @@ -67,7 +67,7 @@ type SetOptions struct { } // BuildXDR for SetOptions returns a fully configured XDR Operation. -func (so *SetOptions) BuildXDR() (xdr.Operation, error) { +func (so *SetOptions) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { err := so.handleInflation() if err != nil { return xdr.Operation{}, errors.Wrap(err, "failed to set inflation destination address") @@ -95,7 +95,11 @@ func (so *SetOptions) BuildXDR() (xdr.Operation, error) { } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, so.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, so.SourceAccount) + } else { + SetOpSourceAccount(&op, so.SourceAccount) + } return op, nil } @@ -293,13 +297,13 @@ func (so *SetOptions) handleSignerXDR(xSigner *xdr.Signer) { } // FromXDR for SetOptions initialises the txnbuild struct from the corresponding xdr Operation. -func (so *SetOptions) FromXDR(xdrOp xdr.Operation) error { +func (so *SetOptions) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { result, ok := xdrOp.Body.GetSetOptionsOp() if !ok { return errors.New("error parsing set_options operation from xdr") } - so.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + so.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) so.handleInflationXDR(result.InflationDest) so.handleClearFlagsXDR(result.ClearFlags) so.handleSetFlagsXDR(result.SetFlags) @@ -315,7 +319,7 @@ func (so *SetOptions) FromXDR(xdrOp xdr.Operation) error { // Validate for SetOptions validates the required struct fields. It returns an error if any // of the fields are invalid. Otherwise, it returns nil. -func (so *SetOptions) Validate() error { +func (so *SetOptions) Validate(withMuxedAccounts bool) error { // skipping checks here because the individual methods above already check for required fields. // Refactoring is out of the scope of this issue(https://github.com/stellar/go/issues/1041) so will leave as is for now. return nil diff --git a/txnbuild/set_options_test.go b/txnbuild/set_options_test.go index 0398f37257..3201065869 100644 --- a/txnbuild/set_options_test.go +++ b/txnbuild/set_options_test.go @@ -131,7 +131,7 @@ func TestEmptyHomeDomainOK(t *testing.T) { options := SetOptions{ HomeDomain: NewHomeDomain(""), } - options.BuildXDR() + options.BuildXDR(false) assert.Equal(t, string(*options.xdrOp.HomeDomain), "", "empty string home domain is set") diff --git a/txnbuild/set_trust_line_flags.go b/txnbuild/set_trust_line_flags.go index 705b6d4310..12c59ab824 100644 --- a/txnbuild/set_trust_line_flags.go +++ b/txnbuild/set_trust_line_flags.go @@ -29,7 +29,7 @@ type SetTrustLineFlags struct { } // BuildXDR for ASetTrustLineFlags returns a fully configured XDR Operation. -func (stf *SetTrustLineFlags) BuildXDR() (xdr.Operation, error) { +func (stf *SetTrustLineFlags) BuildXDR(withMuxedAccounts bool) (xdr.Operation, error) { var xdrOp xdr.SetTrustLineFlagsOp // Set XDR address associated with the trustline @@ -57,7 +57,11 @@ func (stf *SetTrustLineFlags) BuildXDR() (xdr.Operation, error) { return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") } op := xdr.Operation{Body: body} - SetOpSourceAccount(&op, stf.SourceAccount) + if withMuxedAccounts { + SetOpSourceMuxedAccount(&op, stf.SourceAccount) + } else { + SetOpSourceAccount(&op, stf.SourceAccount) + } return op, nil } @@ -70,13 +74,13 @@ func trustLineFlagsToXDR(flags []TrustLineFlag) xdr.Uint32 { } // FromXDR for SetTrustLineFlags initialises the txnbuild struct from the corresponding xdr Operation. -func (stf *SetTrustLineFlags) FromXDR(xdrOp xdr.Operation) error { +func (stf *SetTrustLineFlags) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) error { op, ok := xdrOp.Body.GetSetTrustLineFlagsOp() if !ok { return errors.New("error parsing allow_trust operation from xdr") } - stf.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + stf.SourceAccount = accountFromXDR(xdrOp.SourceAccount, withMuxedAccounts) stf.Trustor = op.Trustor.Address() asset, err := assetFromXDR(op.Asset) if err != nil { @@ -106,7 +110,7 @@ func fromXDRTrustlineFlag(flags xdr.Uint32) []TrustLineFlag { // Validate for SetTrustLineFlags validates the required struct fields. It returns an error if any of the fields are // invalid. Otherwise, it returns nil. -func (stf *SetTrustLineFlags) Validate() error { +func (stf *SetTrustLineFlags) Validate(withMuxedAccounts bool) error { err := validateStellarPublicKey(stf.Trustor) if err != nil { return NewValidationError("Trustor", err.Error()) diff --git a/txnbuild/set_trustline_flags_test.go b/txnbuild/set_trustline_flags_test.go index 7990986a2e..dcf51144aa 100644 --- a/txnbuild/set_trustline_flags_test.go +++ b/txnbuild/set_trustline_flags_test.go @@ -78,17 +78,27 @@ func TestSetTrustLineFlags(t *testing.T) { } { t.Run(testcase.name, func(t *testing.T) { op := testcase.op - assert.NoError(t, op.Validate()) - xdrOp, err := op.BuildXDR() + assert.NoError(t, op.Validate(false)) + xdrOp, err := op.BuildXDR(false) assert.NoError(t, err) xdrBin, err := xdrOp.MarshalBinary() assert.NoError(t, err) var xdrOp2 xdr.Operation assert.NoError(t, xdr.SafeUnmarshal(xdrBin, &xdrOp2)) var op2 SetTrustLineFlags - assert.NoError(t, op2.FromXDR(xdrOp2)) + assert.NoError(t, op2.FromXDR(xdrOp2, false)) assert.Equal(t, op, op2) - testOperationsMarshallingRoundtrip(t, []Operation{&testcase.op}) + testOperationsMarshallingRoundtrip(t, []Operation{&testcase.op}, false) }) } + + // with muxed accounts + setTrustLineFlags := SetTrustLineFlags{ + Trustor: trustor, + Asset: asset, + SetFlags: []TrustLineFlag{TrustLineClawbackEnabled}, + ClearFlags: []TrustLineFlag{TrustLineAuthorized, TrustLineAuthorizedToMaintainLiabilities}, + SourceAccount: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + } + testOperationsMarshallingRoundtrip(t, []Operation{&setTrustLineFlags}, true) } diff --git a/txnbuild/transaction.go b/txnbuild/transaction.go index 813d25dd46..871a2a5089 100644 --- a/txnbuild/transaction.go +++ b/txnbuild/transaction.go @@ -514,18 +514,33 @@ func (t GenericTransaction) FeeBump() (*FeeBumpTransaction, bool) { return t.feeBump, t.feeBump != nil } +type TransactionFromXDROption int + +const ( + TransactionFromXDROptionEnableMuxedAccounts TransactionFromXDROption = iota +) + +func areMuxedAccountsEnabled(options []TransactionFromXDROption) bool { + for _, opt := range options { + if opt == TransactionFromXDROptionEnableMuxedAccounts { + return true + } + } + return false +} + // TransactionFromXDR parses the supplied transaction envelope in base64 XDR // and returns a GenericTransaction instance. -func TransactionFromXDR(txeB64 string) (*GenericTransaction, error) { +func TransactionFromXDR(txeB64 string, options ...TransactionFromXDROption) (*GenericTransaction, error) { var xdrEnv xdr.TransactionEnvelope err := xdr.SafeUnmarshalBase64(txeB64, &xdrEnv) if err != nil { return nil, errors.Wrap(err, "unable to unmarshal transaction envelope") } - return transactionFromParsedXDR(xdrEnv) + return transactionFromParsedXDR(xdrEnv, areMuxedAccountsEnabled(options)) } -func transactionFromParsedXDR(xdrEnv xdr.TransactionEnvelope) (*GenericTransaction, error) { +func transactionFromParsedXDR(xdrEnv xdr.TransactionEnvelope, withMuxedAccounts bool) (*GenericTransaction, error) { var err error newTx := &GenericTransaction{} @@ -534,11 +549,19 @@ func transactionFromParsedXDR(xdrEnv xdr.TransactionEnvelope) (*GenericTransacti innerTx, err = transactionFromParsedXDR(xdr.TransactionEnvelope{ Type: xdr.EnvelopeTypeEnvelopeTypeTx, V1: xdrEnv.FeeBump.Tx.InnerTx.V1, - }) + }, withMuxedAccounts) if err != nil { return newTx, errors.New("could not parse inner transaction") } - feeBumpAccount := xdrEnv.FeeBumpAccount().ToAccountId() + var feeAccount string + if withMuxedAccounts { + feeBumpAccount := xdrEnv.FeeBumpAccount() + feeAccount = feeBumpAccount.Address() + } else { + feeBumpAccount := xdrEnv.FeeBumpAccount().ToAccountId() + feeAccount = feeBumpAccount.Address() + } + newTx.feeBump = &FeeBumpTransaction{ envelope: xdrEnv, // A fee-bump transaction has an effective number of operations equal to one plus the @@ -548,12 +571,19 @@ func transactionFromParsedXDR(xdrEnv xdr.TransactionEnvelope) (*GenericTransacti baseFee: xdrEnv.FeeBumpFee() / int64(len(innerTx.simple.operations)+1), maxFee: xdrEnv.FeeBumpFee(), inner: innerTx.simple, - feeAccount: feeBumpAccount.Address(), + feeAccount: feeAccount, } + return newTx, nil } - - sourceAccount := xdrEnv.SourceAccount().ToAccountId() + var accountID string + if withMuxedAccounts { + sourceAccount := xdrEnv.SourceAccount() + accountID = sourceAccount.Address() + } else { + sourceAccount := xdrEnv.SourceAccount().ToAccountId() + accountID = sourceAccount.Address() + } totalFee := int64(xdrEnv.Fee()) baseFee := totalFee @@ -566,7 +596,7 @@ func transactionFromParsedXDR(xdrEnv xdr.TransactionEnvelope) (*GenericTransacti baseFee: baseFee, maxFee: totalFee, sourceAccount: SimpleAccount{ - AccountID: sourceAccount.Address(), + AccountID: accountID, Sequence: xdrEnv.SeqNum(), }, operations: nil, @@ -585,7 +615,7 @@ func transactionFromParsedXDR(xdrEnv xdr.TransactionEnvelope) (*GenericTransacti operations := xdrEnv.Operations() for _, op := range operations { - newOp, err := operationFromXDR(op) + newOp, err := operationFromXDR(op, withMuxedAccounts) if err != nil { return nil, err } @@ -604,6 +634,7 @@ type TransactionParams struct { BaseFee int64 Memo Memo Timebounds Timebounds + EnableMuxedAccounts bool } // NewTransaction returns a new Transaction instance @@ -634,10 +665,17 @@ func NewTransaction(params TransactionParams) (*Transaction, error) { memo: params.Memo, timebounds: params.Timebounds, } - - accountID, err := xdr.AddressToAccountId(tx.sourceAccount.AccountID) - if err != nil { - return nil, errors.Wrap(err, "account id is not valid") + var sourceAccount xdr.MuxedAccount + if params.EnableMuxedAccounts { + if err = sourceAccount.SetAddress(tx.sourceAccount.AccountID); err != nil { + return nil, errors.Wrap(err, "account id is not valid") + } + } else { + accountID, err2 := xdr.AddressToAccountId(tx.sourceAccount.AccountID) + if err2 != nil { + return nil, errors.Wrap(err2, "account id is not valid") + } + sourceAccount = accountID.ToMuxedAccount() } if tx.baseFee < MinBaseFee { @@ -669,7 +707,7 @@ func NewTransaction(params TransactionParams) (*Transaction, error) { Type: xdr.EnvelopeTypeEnvelopeTypeTx, V1: &xdr.TransactionV1Envelope{ Tx: xdr.Transaction{ - SourceAccount: accountID.ToMuxedAccount(), + SourceAccount: sourceAccount, Fee: xdr.Uint32(tx.maxFee), SeqNum: xdr.SequenceNumber(sequence), TimeBounds: &xdr.TimeBounds{ @@ -691,11 +729,10 @@ func NewTransaction(params TransactionParams) (*Transaction, error) { } for _, op := range tx.operations { - if verr := op.Validate(); verr != nil { + if verr := op.Validate(params.EnableMuxedAccounts); verr != nil { return nil, errors.Wrap(verr, fmt.Sprintf("validation failed for %T operation", op)) } - - xdrOperation, err2 := op.BuildXDR() + xdrOperation, err2 := op.BuildXDR(params.EnableMuxedAccounts) if err2 != nil { return nil, errors.Wrap(err2, fmt.Sprintf("failed to build operation %T", op)) } @@ -709,9 +746,10 @@ func NewTransaction(params TransactionParams) (*Transaction, error) { // FeeBumpTransactionParams is a container for parameters // which are used to construct new FeeBumpTransaction instances type FeeBumpTransactionParams struct { - Inner *Transaction - FeeAccount string - BaseFee int64 + Inner *Transaction + FeeAccount string + BaseFee int64 + EnableMuxedAccounts bool } func convertToV1(tx *Transaction) (*Transaction, error) { @@ -782,16 +820,23 @@ func NewFeeBumpTransaction(params FeeBumpTransactionParams) (*FeeBumpTransaction ) } - accountID, err := xdr.AddressToAccountId(tx.feeAccount) - if err != nil { - return tx, errors.Wrap(err, "fee account is not a valid address") + var feeSource xdr.MuxedAccount + if params.EnableMuxedAccounts { + if err := feeSource.SetAddress(tx.feeAccount); err != nil { + return tx, errors.Wrap(err, "fee account is not a valid address") + } + } else { + accountID, err := xdr.AddressToAccountId(tx.feeAccount) + if err != nil { + return tx, errors.Wrap(err, "fee account is not a valid address") + } + feeSource = accountID.ToMuxedAccount() } - tx.envelope = xdr.TransactionEnvelope{ Type: xdr.EnvelopeTypeEnvelopeTypeTxFeeBump, FeeBump: &xdr.FeeBumpTransactionEnvelope{ Tx: xdr.FeeBumpTransaction{ - FeeSource: accountID.ToMuxedAccount(), + FeeSource: feeSource, Fee: xdr.Int64(tx.maxFee), InnerTx: xdr.FeeBumpTransactionInnerTx{ Type: xdr.EnvelopeTypeEnvelopeTypeTx, diff --git a/txnbuild/transaction_test.go b/txnbuild/transaction_test.go index e85a49ca37..cb26411e55 100644 --- a/txnbuild/transaction_test.go +++ b/txnbuild/transaction_test.go @@ -165,6 +165,76 @@ func TestPayment(t *testing.T) { assert.Equal(t, expected, received, "Base 64 XDR should match") } +func TestPaymentMuxedAccounts(t *testing.T) { + kp0 := newKeypair0() + accountID := xdr.MustAddress(kp0.Address()) + mx := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *accountID.Ed25519, + }, + } + sourceAccount := NewSimpleAccount(mx.Address(), int64(9605939170639898)) + + payment := Payment{ + Destination: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Amount: "10", + Asset: NativeAsset{}, + SourceAccount: sourceAccount.AccountID, + } + + received, err := newSignedTransaction( + TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + Operations: []Operation{&payment}, + BaseFee: MinBaseFee, + Timebounds: NewInfiniteTimeout(), + EnableMuxedAccounts: true, + }, + network.TestNetworkPassphrase, + kp0, + ) + assert.NoError(t, err) + + expected := "AAAAAgAAAQAAAAAAyv66vuDcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAEAAAAAAMr+ur7g3G3hclysZlFitS+s5zWyiiJD5B0STWy5LXCj6i5yxQAAAAEAAAEAgAAAAAAAAAA/DDS/k60NmXHQTMyQ9wVRHIOKrZc0pKL7DXoD/H/omgAAAAAAAAAABfXhAAAAAAAAAAAB6i5yxQAAAED4Wkvwf/BJV+fqa6Kvi+T/7ZL82pOinN68GlvEi9qK4klH+qITyvN3jRj5Nfz0+VrE2xBJPVc8sS/qN9LlznoC" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestPaymentFailsMuxedAccountsIfNotEnabled(t *testing.T) { + kp0 := newKeypair0() + accountID := xdr.MustAddress(kp0.Address()) + mx := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *accountID.Ed25519, + }, + } + sourceAccount := NewSimpleAccount(mx.Address(), int64(9605939170639898)) + + payment := Payment{ + Destination: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", + Amount: "10", + Asset: NativeAsset{}, + } + + _, err := newSignedTransaction( + TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + Operations: []Operation{&payment}, + BaseFee: MinBaseFee, + Timebounds: NewInfiniteTimeout(), + EnableMuxedAccounts: false, + }, + network.TestNetworkPassphrase, + kp0, + ) + assert.Error(t, err) +} + func TestPaymentFailsIfNoAssetSpecified(t *testing.T) { kp0 := newKeypair0() sourceAccount := NewSimpleAccount(kp0.Address(), int64(9605939170639898)) @@ -1316,6 +1386,32 @@ func TestFromXDR(t *testing.T) { assert.Equal(t, "", op2.SourceAccount, "Operation source should match") assert.Equal(t, "test", op2.Name, "Name should match") assert.Equal(t, "value", string(op2.Value), "Value should match") + + // Muxed accounts + txB64WithMuxedAccounts := "AAAAAgAAAQAAAAAAyv66vuDcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAEAAAAAAMr+ur7g3G3hclysZlFitS+s5zWyiiJD5B0STWy5LXCj6i5yxQAAAAEAAAEAgAAAAAAAAAA/DDS/k60NmXHQTMyQ9wVRHIOKrZc0pKL7DXoD/H/omgAAAAAAAAAABfXhAAAAAAAAAAAB6i5yxQAAAED4Wkvwf/BJV+fqa6Kvi+T/7ZL82pOinN68GlvEi9qK4klH+qITyvN3jRj5Nfz0+VrE2xBJPVc8sS/qN9LlznoC" + + // It provides M-addreses when enabling muxed accounts + tx3, err := TransactionFromXDR(txB64WithMuxedAccounts, TransactionFromXDROptionEnableMuxedAccounts) + assert.NoError(t, err) + newTx3, ok := tx3.Transaction() + assert.True(t, ok) + assert.Equal(t, "MDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKAAAAAAMV7V2XYGQO", newTx3.sourceAccount.AccountID) + op3, ok3 := newTx3.Operations()[0].(*Payment) + assert.True(t, ok3) + assert.Equal(t, "MDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKAAAAAAMV7V2XYGQO", op3.SourceAccount) + assert.Equal(t, "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK", op3.Destination) + + // It does provide G-addreses when not enabling muxed accounts + tx3, err = TransactionFromXDR(txB64WithMuxedAccounts) + assert.NoError(t, err) + newTx3, ok = tx3.Transaction() + assert.True(t, ok) + assert.Equal(t, "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", newTx3.sourceAccount.AccountID) + op3, ok3 = newTx3.Operations()[0].(*Payment) + assert.True(t, ok3) + assert.Equal(t, "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", op3.SourceAccount) + assert.Equal(t, "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ", op3.Destination) + } func TestBuild(t *testing.T) { diff --git a/xdr/muxed_account.go b/xdr/muxed_account.go index 3a9e7c31fa..5b6500a50a 100644 --- a/xdr/muxed_account.go +++ b/xdr/muxed_account.go @@ -1,9 +1,9 @@ package xdr import ( - "errors" "fmt" + "github.com/pkg/errors" "github.com/stellar/go/strkey" ) @@ -21,9 +21,9 @@ func MustMuxedAddressPtr(address string) *MuxedAccount { return &muxed } -// SetAddress modifies the receiver, setting it's value to the MuxedAccount form -// of the provided address. -func (m *MuxedAccount) SetAddress(address string) error { +// SetEd25519Address modifies the receiver, setting it's value to the MuxedAccount form +// of the provided G-address. Unlike SetAddress(), it only supports G-addresses. +func (m *MuxedAccount) SetEd25519Address(address string) error { if m == nil { return nil } @@ -35,14 +35,103 @@ func (m *MuxedAccount) SetAddress(address string) error { return err } if len(raw) != 32 { - return errors.New("invalid address") + return fmt.Errorf("invalid binary length: %d", len(raw)) } var ui Uint256 copy(ui[:], raw) *m, err = NewMuxedAccount(CryptoKeyTypeKeyTypeEd25519, ui) return err default: - return errors.New("invalid address") + return errors.New("invalid address length") + } + +} + +// SetAddress modifies the receiver, setting it's value to the MuxedAccount form +// of the provided strkey G-address or M-address, as described in SEP23. +func (m *MuxedAccount) SetAddress(address string) error { + if m == nil { + return nil + } + + switch len(address) { + case 56: + return m.SetEd25519Address(address) + case 69: + raw, err := strkey.Decode(strkey.VersionByteMuxedAccount, address) + if err != nil { + return err + } + if len(raw) != 40 { + return fmt.Errorf("invalid binary length: %d", len(raw)) + } + var muxed MuxedAccountMed25519 + copy(muxed.Ed25519[:], raw[:32]) + if err = muxed.Id.UnmarshalBinary(raw[32:]); err != nil { + return err + } + *m, err = NewMuxedAccount(CryptoKeyTypeKeyTypeMuxedEd25519, muxed) + return err + default: + return errors.New("invalid address length") + } + +} + +// AddressToMuxedAccount returns an MuxedAccount for a given address string +// or SEP23 M-address. +// If the address is not valid the error returned will not be nil +func AddressToMuxedAccount(address string) (MuxedAccount, error) { + result := MuxedAccount{} + err := result.SetAddress(address) + + return result, err +} + +// Address returns the strkey-encoded form of this MuxedAccount. In particular, it will +// return an M- strkey representation for CryptoKeyTypeKeyTypeMuxedEd25519 variants of the account +// (according to SEP23). This method will panic if the MuxedAccount is backed by a public key of an +// unknown type. +func (m *MuxedAccount) Address() string { + address, err := m.GetAddress() + if err != nil { + panic(err) + } + return address +} + +// GetAddress returns the strkey-encoded form of this MuxedAccount. In particular, it will +// return an M-strkey representation for CryptoKeyTypeKeyTypeMuxedEd25519 variants of the account +// (according to SEP23). In addition it will return an error if the MuxedAccount is backed by a +// public key of an unknown type. +func (m *MuxedAccount) GetAddress() (string, error) { + if m == nil { + return "", nil + } + + raw := make([]byte, 0, 40) + switch m.Type { + case CryptoKeyTypeKeyTypeEd25519: + ed, ok := m.GetEd25519() + if !ok { + return "", errors.New("could not get Ed25519") + } + raw = append(raw, ed[:]...) + return strkey.Encode(strkey.VersionByteAccountID, raw) + case CryptoKeyTypeKeyTypeMuxedEd25519: + ed, ok := m.GetMed25519() + if !ok { + return "", errors.New("could not get Med25519") + } + idBytes, err := ed.Id.MarshalBinary() + if err != nil { + return "", errors.Wrap(err, "could not marshal ID") + } + raw = append(raw, ed.Ed25519[:]...) + raw = append(raw, idBytes...) + return strkey.Encode(strkey.VersionByteMuxedAccount, raw) + default: + return "", fmt.Errorf("Unknown muxed account type: %v", m.Type) } } diff --git a/xdr/muxed_account_test.go b/xdr/muxed_account_test.go index 136ee7498d..47cc3a43f6 100644 --- a/xdr/muxed_account_test.go +++ b/xdr/muxed_account_test.go @@ -8,43 +8,100 @@ import ( ) var _ = Describe("xdr.MuxedAccount#Get/SetAddress()", func() { + It("returns an empty string when muxed account is nil", func() { + addy := (*MuxedAccount)(nil).Address() + Expect(addy).To(Equal("")) + }) + + It("returns a strkey string when muxed account is valid", func() { + var unmuxed MuxedAccount + err := unmuxed.SetEd25519Address("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ") + Expect(err).ShouldNot(HaveOccurred()) + Expect(unmuxed.Type).To(Equal(CryptoKeyTypeKeyTypeEd25519)) + Expect(*unmuxed.Ed25519).To(Equal(Uint256{63, 12, 52, 191, 147, 173, 13, 153, 113, 208, 76, 204, 144, 247, 5, 81, 28, 131, 138, 173, 151, 52, 164, 162, 251, 13, 122, 3, 252, 127, 232, 154})) + muxedy := unmuxed.Address() + Expect(muxedy).To(Equal("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ")) + + var muxed MuxedAccount + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK") + Expect(err).ShouldNot(HaveOccurred()) + Expect(muxed.Type).To(Equal(CryptoKeyTypeKeyTypeMuxedEd25519)) + Expect(muxed.Med25519.Id).To(Equal(Uint64(9223372036854775808))) + Expect(muxed.Med25519.Ed25519).To(Equal(*unmuxed.Ed25519)) + muxedy = muxed.Address() + Expect(muxedy).To(Equal("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK")) + + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ") + Expect(err).ShouldNot(HaveOccurred()) + Expect(muxed.Type).To(Equal(CryptoKeyTypeKeyTypeMuxedEd25519)) + Expect(muxed.Med25519.Id).To(Equal(Uint64(0))) + Expect(muxed.Med25519.Ed25519).To(Equal(*unmuxed.Ed25519)) + muxedy = muxed.Address() + Expect(muxedy).To(Equal("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ")) + }) It("returns an error when the strkey is invalid", func() { var muxed MuxedAccount // Test cases from SEP23 - err := muxed.SetAddress("GAAAAAAAACGC6") + err := muxed.SetEd25519Address("GAAAAAAAACGC6") + Expect(err).Should(HaveOccurred()) + + err = muxed.SetEd25519Address("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUR") + Expect(err).Should(HaveOccurred()) + + err = muxed.SetEd25519Address("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZA") + Expect(err).Should(HaveOccurred()) + + err = muxed.SetEd25519Address("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUACUSI") + Expect(err).Should(HaveOccurred()) + + err = muxed.SetEd25519Address("G47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVP2I") + Expect(err).Should(HaveOccurred()) + + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLKA") + Expect(err).Should(HaveOccurred()) + + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAAV75I") Expect(err).Should(HaveOccurred()) - err = muxed.SetAddress("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZA") + err = muxed.SetAddress("M47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ") Expect(err).Should(HaveOccurred()) - err = muxed.SetAddress("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUACUSI") + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUK===") Expect(err).Should(HaveOccurred()) - err = muxed.SetAddress("G47QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVP2I") + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUO") Expect(err).Should(HaveOccurred()) + }) +}) + +var _ = Describe("xdr.AddressToMuxedAccount()", func() { + It("works", func() { + address := "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ" + muxedAccount, err := AddressToMuxedAccount(address) + + Expect(muxedAccount.Address()).To(Equal("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ")) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = AddressToAccountId("GCR22L3") + Expect(err).Should(HaveOccurred()) }) }) var _ = Describe("xdr.MuxedAccount.ToAccountId()", func() { It("works", func() { var muxed MuxedAccount - err := muxed.SetAddress("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ") + + err := muxed.SetEd25519Address("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ") Expect(err).ShouldNot(HaveOccurred()) aid := muxed.ToAccountId() Expect(aid.Address()).To(Equal("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ")) - muxed = MuxedAccount{ - Type: CryptoKeyTypeKeyTypeMuxedEd25519, - Med25519: &MuxedAccountMed25519{ - Id: 0xcafebabe, - Ed25519: *muxed.Ed25519, - }, - } - + err = muxed.SetAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK") + Expect(err).ShouldNot(HaveOccurred()) aid = muxed.ToAccountId() Expect(aid.Address()).To(Equal("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ")) })