Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4728: include contract asset balance changes in payments #4807

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions protocols/horizon/operations/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,23 @@ type LiquidityPoolWithdraw struct {
// InvokeHostFunction is the json resource representing a single smart contract
// function invocation operation, having type InvokeHostFunction.
//
// The model for InvokeHostFunction is intentionally simplified, Footprint
// just contains a base64 encoded string of it's xdr serialization.
// The model for InvokeHostFunction is intentionally simplified.
// Parameters - array of tuples of each function input parameter value and it's data type
// Function - name of contract function
// Footprint - base64 encoded string of it's xdr serialization.
// AssetBalanceChanges - array of asset balance changed records.
// The asset balance change record is captured at ingestion time from the asset contract
// events present in tx meta. Only asset contract events that have a reference to classic account in
// either the 'from' or 'to' participants will be included here as an asset balance change.
// Any pure contract-to-contract events with no reference to classic accounts are not included,
// as there is no explicit model in horizon for contract addresses yet.

type InvokeHostFunction struct {
sreuland marked this conversation as resolved.
Show resolved Hide resolved
Base
Parameters []HostFunctionParameter `json:"parameters"`
Function string `json:"function"`
Footprint string `json:"footprint"`
Parameters []HostFunctionParameter `json:"parameters"`
Function string `json:"function"`
Footprint string `json:"footprint"`
AssetBalanceChanges []AssetContractBalanceChange `json:"asset_balance_changes"`
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
}

// InvokeHostFunction parameter model, intentionally simplified, Value
Expand All @@ -364,6 +374,21 @@ type HostFunctionParameter struct {
Type string `json:"type"`
}

// Type - refers to the source SAC Event
// it can only be one of 'transfer', 'mint', 'clawback' or 'burn'
// From - this is classic account that asset balance was changed.
// To - this is the classic account that asset balance was changed, or if not applicable
// for asset contract event type, it can be absent such as 'burn'
// Amount - expressed as a signed decimal to 7 digits precision.
// Asset - the classic asset expressed as issuer and code.
type AssetContractBalanceChange struct {
base.Asset
Type string `json:"type"`
From string `json:"from"`
To string `json:"to,omitempty"`
Amount string `json:"amount"`
}

// Operation interface contains methods implemented by the operation types
type Operation interface {
GetBase() Base
Expand Down
134 changes: 134 additions & 0 deletions services/horizon/internal/actions/operation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,148 @@ import (
"testing"
"time"

"github.com/guregu/null"
"github.com/stellar/go/protocols/horizon/operations"
"github.com/stellar/go/services/horizon/internal/db2/history"
"github.com/stellar/go/services/horizon/internal/ledger"
"github.com/stellar/go/services/horizon/internal/render/problem"
"github.com/stellar/go/services/horizon/internal/test"
supportProblem "github.com/stellar/go/support/render/problem"
"github.com/stellar/go/toid"
"github.com/stellar/go/xdr"
)

func TestInvokeHostFnDetailsInPaymentOperations(t *testing.T) {
tt := test.Start(t)
defer tt.Finish()
test.ResetHorizonDB(t, tt.HorizonDB)

q := &history.Q{tt.HorizonSession()}
handler := GetOperationsHandler{OnlyPayments: true}

txIndex := int32(1)
sequence := int32(56)
txID := toid.New(sequence, txIndex, 0).ToInt64()
opID1 := toid.New(sequence, txIndex, 1).ToInt64()

ledgerCloseTime := time.Now().Unix()
_, err := q.InsertLedger(tt.Ctx, xdr.LedgerHeaderHistoryEntry{
Header: xdr.LedgerHeader{
LedgerSeq: xdr.Uint32(sequence),
ScpValue: xdr.StellarValue{
CloseTime: xdr.TimePoint(ledgerCloseTime),
},
},
}, 1, 0, 1, 0, 0)
tt.Assert.NoError(err)

transactionBuilder := q.NewTransactionBatchInsertBuilder(1)
firstTransaction := buildLedgerTransaction(tt.T, testTransaction{
index: uint32(txIndex),
envelopeXDR: "AAAAACiSTRmpH6bHC6Ekna5e82oiGY5vKDEEUgkq9CB//t+rAAAAyAEXUhsAADDRAAAAAAAAAAAAAAABAAAAAAAAAAsBF1IbAABX4QAAAAAAAAAA",
resultXDR: "AAAAAAAAASwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAFAAAAAAAAAAA=",
feeChangesXDR: "AAAAAA==",
metaXDR: "AAAAAQAAAAAAAAAA",
hash: "19aaa18db88605aedec04659fb45e06f240b022eb2d429e05133e4d53cd945ba",
})
err = transactionBuilder.Add(tt.Ctx, firstTransaction, uint32(sequence))
tt.Assert.NoError(err)

operationBuilder := q.NewOperationBatchInsertBuilder(1)
err = operationBuilder.Add(tt.Ctx,
opID1,
txID,
1,
xdr.OperationTypeInvokeHostFunction,
[]byte(`{
"parameters": [],
"function": "fn",
"footprint": "",
"asset_balance_changes": [
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "contract",
"to": "G_FOR_CLASSIC_ACCOUNT_ADDRESS1",
"amount": "3",
"type": "transfer"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "G_FOR_CLASSIC_ACCOUNT_ADDRESS2",
"to": "G_FOR_CLASSIC_ACCOUNT_ADDRESS3",
"amount": "5",
"type": "clawback"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "G_FOR_CLASSIC_ACCOUNT_ADDRESS2",
"amount": "6",
"type": "burn"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "G_FOR_CLASSIC_ACCOUNT_ADDRESS2",
"to": "contract",
"amount": "10",
"type": "mint"
}
]
}`),
"GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY",
null.String{},
true)
tt.Assert.NoError(err)

records, err := handler.GetResourcePage(
httptest.NewRecorder(),
makeRequest(
t, map[string]string{}, map[string]string{}, q,
),
)
tt.Assert.NoError(err)
tt.Assert.Len(records, 1)

op := records[0].(operations.InvokeHostFunction)
tt.Assert.Equal(op.Function, "fn")
tt.Assert.Equal(len(op.AssetBalanceChanges), 4)
tt.Assert.Equal(op.AssetBalanceChanges[0].From, "contract")
tt.Assert.Equal(op.AssetBalanceChanges[0].To, "G_FOR_CLASSIC_ACCOUNT_ADDRESS1")
tt.Assert.Equal(op.AssetBalanceChanges[0].Amount, "3")
tt.Assert.Equal(op.AssetBalanceChanges[0].Type, "transfer")
tt.Assert.Equal(op.AssetBalanceChanges[0].Asset.Type, "credit_alphanum4")
tt.Assert.Equal(op.AssetBalanceChanges[0].Asset.Code, "abc")
tt.Assert.Equal(op.AssetBalanceChanges[0].Asset.Issuer, "123")
tt.Assert.Equal(op.AssetBalanceChanges[1].From, "G_FOR_CLASSIC_ACCOUNT_ADDRESS2")
tt.Assert.Equal(op.AssetBalanceChanges[1].To, "G_FOR_CLASSIC_ACCOUNT_ADDRESS3")
tt.Assert.Equal(op.AssetBalanceChanges[1].Amount, "5")
tt.Assert.Equal(op.AssetBalanceChanges[1].Type, "clawback")
tt.Assert.Equal(op.AssetBalanceChanges[1].Asset.Type, "credit_alphanum4")
tt.Assert.Equal(op.AssetBalanceChanges[1].Asset.Code, "abc")
tt.Assert.Equal(op.AssetBalanceChanges[1].Asset.Issuer, "123")
tt.Assert.Equal(op.AssetBalanceChanges[2].From, "G_FOR_CLASSIC_ACCOUNT_ADDRESS2")
tt.Assert.Equal(op.AssetBalanceChanges[2].To, "")
tt.Assert.Equal(op.AssetBalanceChanges[2].Amount, "6")
tt.Assert.Equal(op.AssetBalanceChanges[2].Type, "burn")
tt.Assert.Equal(op.AssetBalanceChanges[2].Asset.Type, "credit_alphanum4")
tt.Assert.Equal(op.AssetBalanceChanges[2].Asset.Code, "abc")
tt.Assert.Equal(op.AssetBalanceChanges[2].Asset.Issuer, "123")
tt.Assert.Equal(op.AssetBalanceChanges[3].From, "G_FOR_CLASSIC_ACCOUNT_ADDRESS2")
tt.Assert.Equal(op.AssetBalanceChanges[3].To, "contract")
tt.Assert.Equal(op.AssetBalanceChanges[3].Amount, "10")
tt.Assert.Equal(op.AssetBalanceChanges[3].Type, "mint")
tt.Assert.Equal(op.AssetBalanceChanges[3].Asset.Type, "credit_alphanum4")
tt.Assert.Equal(op.AssetBalanceChanges[3].Asset.Code, "abc")
tt.Assert.Equal(op.AssetBalanceChanges[3].Asset.Issuer, "123")
}

func TestGetOperationsWithoutFilter(t *testing.T) {
tt := test.Start(t)
defer tt.Finish()
Expand Down
45 changes: 45 additions & 0 deletions services/horizon/internal/actions/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package actions

import (
"encoding/hex"
"testing"

"github.com/stellar/go/ingest"
"github.com/stellar/go/xdr"
"github.com/stretchr/testify/assert"
)

type testTransaction struct {
index uint32
envelopeXDR string
resultXDR string
feeChangesXDR string
metaXDR string
hash string
}

func buildLedgerTransaction(t *testing.T, tx testTransaction) ingest.LedgerTransaction {
transaction := ingest.LedgerTransaction{
Index: tx.index,
Envelope: xdr.TransactionEnvelope{},
Result: xdr.TransactionResultPair{},
FeeChanges: xdr.LedgerEntryChanges{},
UnsafeMeta: xdr.TransactionMeta{},
}

tt := assert.New(t)

err := xdr.SafeUnmarshalBase64(tx.envelopeXDR, &transaction.Envelope)
tt.NoError(err)
err = xdr.SafeUnmarshalBase64(tx.resultXDR, &transaction.Result.Result)
tt.NoError(err)
err = xdr.SafeUnmarshalBase64(tx.metaXDR, &transaction.UnsafeMeta)
tt.NoError(err)
err = xdr.SafeUnmarshalBase64(tx.feeChangesXDR, &transaction.FeeChanges)
tt.NoError(err)

_, err = hex.Decode(transaction.Result.TransactionHash[:], []byte(tx.hash))
tt.NoError(err)

return transaction
}
1 change: 1 addition & 0 deletions services/horizon/internal/db2/history/fee_bump_scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ func FeeBumpScenario(tt *test.T, q *Q, successful bool) FeeBumpFixture {
details,
account.Address(),
null.String{},
false,
))
tt.Assert.NoError(opBuilder.Exec(ctx))

Expand Down
1 change: 1 addition & 0 deletions services/horizon/internal/db2/history/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ type Operation struct {
SourceAccount string `db:"source_account"`
SourceAccountMuxed null.String `db:"source_account_muxed"`
TransactionSuccessful bool `db:"transaction_successful"`
IsPayment bool `db:"is_payment"`
}

// ManageOffer is a struct of data from `operations.DetailsString`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (m *MockOperationsBatchInsertBuilder) Add(ctx context.Context,
details []byte,
sourceAccount string,
sourceAccountMuxed null.String,
isPayment bool,
) error {
a := m.Called(ctx,
id,
Expand All @@ -31,6 +32,7 @@ func (m *MockOperationsBatchInsertBuilder) Add(ctx context.Context,
details,
sourceAccount,
sourceAccountMuxed,
isPayment,
)
return a.Error(0)
}
Expand Down
23 changes: 14 additions & 9 deletions services/horizon/internal/db2/history/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,16 +235,20 @@ func (q *OperationsQ) ForTransaction(ctx context.Context, hash string) *Operatio
}

// OnlyPayments filters the query being built to only include operations that
// are in the "payment" class of operations: CreateAccountOps, Payments, and
// PathPayments.
// are in the "payment" class of classic operations: CreateAccountOps, Payments, and
// PathPayments. OR also includes contract asset balance changes as expressed in 'is_payment' flag
// on the history operations table.
func (q *OperationsQ) OnlyPayments() *OperationsQ {
q.sql = q.sql.Where(sq.Eq{"hop.type": []xdr.OperationType{
xdr.OperationTypeCreateAccount,
xdr.OperationTypePayment,
xdr.OperationTypePathPaymentStrictReceive,
xdr.OperationTypePathPaymentStrictSend,
xdr.OperationTypeAccountMerge,
}})
q.sql = q.sql.Where(sq.Or{
sq.Eq{"hop.type": []xdr.OperationType{
xdr.OperationTypeCreateAccount,
xdr.OperationTypePayment,
xdr.OperationTypePathPaymentStrictReceive,
xdr.OperationTypePathPaymentStrictSend,
xdr.OperationTypeAccountMerge,
}},
sq.Eq{"hop.is_payment": true}})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should test the performance of this new query on staging. If it is too slow we could try doing two queries. The first query would check hop.is_payment is true and if we have a full page of results we can stop there. Otherwise, we can do a second query based of the hop.type column.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, on staging, there won't be many rows with is_payment being non-null yet to begin with, just forward ingestion history after the version is installed, great suggestion, I need to confirm though on the core ingestion config, b/c this version requires the soroban enabled core and staging has config for the non-soroban core.


return q
}

Expand Down Expand Up @@ -390,6 +394,7 @@ var selectOperation = sq.Select(
"hop.details, " +
"hop.source_account, " +
"hop.source_account_muxed, " +
"hop.is_payment, " +
"ht.transaction_hash, " +
"ht.tx_result, " +
"COALESCE(ht.successful, true) as transaction_successful").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type OperationBatchInsertBuilder interface {
details []byte,
sourceAccount string,
sourceAcccountMuxed null.String,
isPayment bool,
) error
Exec(ctx context.Context) error
}
Expand Down Expand Up @@ -49,6 +50,7 @@ func (i *operationBatchInsertBuilder) Add(
details []byte,
sourceAccount string,
sourceAccountMuxed null.String,
isPayment bool,
) error {
return i.builder.Row(ctx, map[string]interface{}{
"id": id,
Expand All @@ -58,6 +60,7 @@ func (i *operationBatchInsertBuilder) Add(
"details": details,
"source_account": sourceAccount,
"source_account_muxed": sourceAccountMuxed,
"is_payment": isPayment,
})

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func TestAddOperation(t *testing.T) {
details,
sourceAccount,
null.StringFrom(sourceAccountMuxed),
true,
)
tt.Assert.NoError(err)

Expand Down
Loading