Skip to content

Commit

Permalink
txnbuild: Calculate claimable balance IDs without submitting transact…
Browse files Browse the repository at this point in the history
…ions. (#3122)

Balance IDs are a crucial piece of information when dealing with claimable
balances. They are highly predictable by design, and so SDK users shouldn't need
to submit the transaction to determine the ID.

A new function returns the claimable balance ID for the particular operation
within the transaction, assuming it is of the type CREATE_CLAIMABLE_BALANCE.

    func (t *Transaction) ClaimableBalanceID(operationIndex int) (string, err) {}

It also introduces an integration test to confirm this behavior. Since it's
appended to an existing test, this only adds a few extra seconds of running
time.

Co-authored-by: Howard Tinghao Chen <[email protected]>
Co-authored-by: Leigh McCulloch <[email protected]>
  • Loading branch information
3 people authored Oct 14, 2020
1 parent 0d4bef8 commit e016d0f
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 16 deletions.
73 changes: 59 additions & 14 deletions services/horizon/internal/integration/protocol14_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

sdk "github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/keypair"
proto "github.com/stellar/go/protocols/horizon"
"github.com/stellar/go/protocols/horizon/operations"
"github.com/stellar/go/services/horizon/internal/codes"
Expand All @@ -20,25 +21,69 @@ var protocol14Config = integration.Config{ProtocolVersion: 14}

func TestProtocol14Basics(t *testing.T) {
tt := assert.New(t)

itest := integration.NewTest(t, protocol14Config)
master := itest.Master()

root, err := itest.Client().Root()
tt.NoError(err)
tt.Equal(int32(14), root.CoreSupportedProtocolVersion)
tt.Equal(int32(14), root.CurrentProtocolVersion)
t.Run("Sanity", func(t *testing.T) {
root, err := itest.Client().Root()
tt.NoError(err)
tt.Equal(int32(14), root.CoreSupportedProtocolVersion)
tt.Equal(int32(14), root.CurrentProtocolVersion)

// Submit a simple tx
op := txnbuild.Payment{
Destination: master.Address(),
Amount: "10",
Asset: txnbuild.NativeAsset{},
}

// Submit a simple tx
op := txnbuild.Payment{
Destination: master.Address(),
Amount: "10",
Asset: txnbuild.NativeAsset{},
}
txResp := itest.MustSubmitOperations(itest.MasterAccount(), master, &op)
tt.Equal(master.Address(), txResp.Account)
tt.Equal("1", txResp.AccountSequence)
})

// Ensure predicting claimable balances works.
t.Run("BalanceIDs", func(t *testing.T) {
tx, err := itest.CreateSignedTransaction(
itest.MasterAccount(),
[]*keypair.Full{master},
&txnbuild.CreateClaimableBalance{
Destinations: []txnbuild.Claimant{
txnbuild.NewClaimant(master.Address(), nil),
},
Asset: txnbuild.NativeAsset{},
Amount: "42",
},
&txnbuild.CreateClaimableBalance{
Destinations: []txnbuild.Claimant{
txnbuild.NewClaimant(master.Address(), nil),
},
Asset: txnbuild.NativeAsset{},
Amount: "24",
})
tt.NoError(err)

txResp := itest.MustSubmitOperations(itest.MasterAccount(), master, &op)
tt.Equal(master.Address(), txResp.Account)
tt.Equal("1", txResp.AccountSequence)
id1, err := tx.ClaimableBalanceID(0)
tt.NoError(err)
id2, err := tx.ClaimableBalanceID(1)
tt.NoError(err)
predictions := []string{id1, id2}

var txResult xdr.TransactionResult
txResp, err := itest.SubmitTransaction(tx)
tt.NoError(err)
xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult)
opResults, ok := txResult.OperationResults()
tt.True(ok)
tt.Len(opResults, len(predictions))

for i, predictedId := range predictions {
claimCreationOp := opResults[i].MustTr().CreateClaimableBalanceResult
calculatedId, err := xdr.MarshalHex(claimCreationOp.BalanceId)
tt.NoError(err)
tt.Equal(predictedId, calculatedId)
}
})
}

func TestHappyClaimableBalances(t *testing.T) {
Expand Down
18 changes: 16 additions & 2 deletions services/horizon/internal/test/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,16 @@ func (i *Test) SubmitOperations(
func (i *Test) SubmitMultiSigOperations(
source txnbuild.Account, signers []*keypair.Full, ops ...txnbuild.Operation,
) (proto.Transaction, error) {
tx, err := i.CreateSignedTransaction(source, signers, ops...)
if err != nil {
return proto.Transaction{}, err
}
return i.SubmitTransaction(tx)
}

func (i *Test) CreateSignedTransaction(
source txnbuild.Account, signers []*keypair.Full, ops ...txnbuild.Operation,
) (*txnbuild.Transaction, error) {
txParams := txnbuild.TransactionParams{
SourceAccount: source,
Operations: ops,
Expand All @@ -579,16 +589,20 @@ func (i *Test) SubmitMultiSigOperations(

tx, err := txnbuild.NewTransaction(txParams)
if err != nil {
return proto.Transaction{}, err
return nil, err
}

for _, signer := range signers {
tx, err = tx.Sign(NetworkPassphrase, signer)
if err != nil {
return proto.Transaction{}, err
return nil, err
}
}

return tx, nil
}

func (i *Test) SubmitTransaction(tx *txnbuild.Transaction) (proto.Transaction, error) {
txb64, err := tx.Base64()
if err != nil {
return proto.Transaction{}, err
Expand Down
2 changes: 2 additions & 0 deletions txnbuild/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
* Add helper function `ParseAssetString()`, making it easier to build an `Asset` structure from a string in [canonical form](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md#asset) and check its various properties ([#3105](https://github.com/stellar/go/pull/3105)).

* Add helper function `Transaction.ClaimableBalanceID()`, making it easier to calculate balance IDs for [claimable balances](https://developers.stellar.org/docs/glossary/claimable-balance/) without actually submitting the transaction ([#3122](https://github.com/stellar/go/pull/3122)).

## [v4.0.1](https://github.com/stellar/go/releases/tag/horizonclient-v4.0.1) - 2020-10-02

* Fixed bug in `TransactionFromXDR()` which occurs when parsing transaction XDR envelopes which contain Protocol 14 operations.
Expand Down
56 changes: 56 additions & 0 deletions txnbuild/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,62 @@ func (t *Transaction) Base64() (string, error) {
return marshallBase64(t.envelope, t.signatures)
}

// ClaimableBalanceID returns the claimable balance ID for the operation at the given index within the transaction.
// given index (which should be a `CreateClaimableBalance` operation).
func (t *Transaction) ClaimableBalanceID(operationIndex int) (string, error) {
if operationIndex < 0 || operationIndex >= len(t.operations) {
return "", errors.New("invalid operation index")
}

operation, ok := t.operations[operationIndex].(*CreateClaimableBalance)
if !ok {
return "", errors.New("operation is not CreateClaimableBalance")
}

// Use the operation's source account or the transaction's source if not.
var account Account = &t.sourceAccount
if operation.SourceAccount != nil {
account = operation.GetSourceAccount()
}

seq, err := account.GetSequenceNumber()
if err != nil {
return "", errors.Wrap(err, "failed to retrieve account sequence number")
}

// We mimic the relevant code from Stellar Core
// https://github.com/stellar/stellar-core/blob/9f3cc04e6ec02c38974c42545a86cdc79809252b/src/test/TestAccount.cpp#L285
operationId := xdr.OperationId{
Type: xdr.EnvelopeTypeEnvelopeTypeOpId,
Id: &xdr.OperationIdId{
SourceAccount: xdr.MustMuxedAddress(account.GetAccountID()),
SeqNum: xdr.SequenceNumber(seq),
OpNum: xdr.Uint32(operationIndex),
},
}

binaryDump, err := operationId.MarshalBinary()
if err != nil {
return "", errors.Wrap(err, "invalid claimable balance operation")
}

hash := sha256.Sum256(binaryDump)
balanceIdXdr, err := xdr.NewClaimableBalanceId(
// TODO: look into whether this be determined programmatically from the operation structure.
xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0,
xdr.Hash(hash))
if err != nil {
return "", errors.Wrap(err, "unable to parse balance ID as XDR")
}

balanceIdHex, err := xdr.MarshalHex(balanceIdXdr)
if err != nil {
return "", errors.Wrap(err, "unable to encode balance ID as hex")
}

return balanceIdHex, nil
}

// FeeBumpTransaction represents a CAP 15 fee bump transaction.
// Fee bump transactions allow an arbitrary account to pay the fee for a transaction.
type FeeBumpTransaction struct {
Expand Down

0 comments on commit e016d0f

Please sign in to comment.