Skip to content

Commit

Permalink
#4728: include contract asset balance changes in payments (#4807)
Browse files Browse the repository at this point in the history
  • Loading branch information
sreuland authored Mar 22, 2023
1 parent 107d5d1 commit 380355e
Show file tree
Hide file tree
Showing 29 changed files with 638 additions and 130 deletions.
39 changes: 34 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 {
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"`
}

// InvokeHostFunction parameter model, intentionally simplified, Value
Expand All @@ -364,6 +374,25 @@ 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
172 changes: 172 additions & 0 deletions services/horizon/internal/actions/operation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,156 @@ package actions

import (
"database/sql"
"encoding/hex"
"fmt"
"net/http/httptest"
"testing"
"time"

"github.com/guregu/null"
"github.com/stellar/go/ingest"
"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"
"github.com/stretchr/testify/assert"
)

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": "C_CONTRACT_ADDRESS1",
"to": "G_CLASSIC_ADDRESS1",
"amount": "3",
"type": "transfer"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "G_CLASSIC_ADDRESS2",
"to": "G_CLASSIC_ADDRESS3",
"amount": "5",
"type": "clawback"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "G_CLASSIC_ADDRESS2",
"amount": "6",
"type": "burn"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "abc",
"asset_issuer": "123",
"from": "G_CLASSIC_ADDRESS2",
"to": "C_CONTRACT_ADDRESS3",
"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, "C_CONTRACT_ADDRESS1")
tt.Assert.Equal(op.AssetBalanceChanges[0].To, "G_CLASSIC_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_CLASSIC_ADDRESS2")
tt.Assert.Equal(op.AssetBalanceChanges[1].To, "G_CLASSIC_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_CLASSIC_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_CLASSIC_ADDRESS2")
tt.Assert.Equal(op.AssetBalanceChanges[3].To, "C_CONTRACT_ADDRESS3")
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 Expand Up @@ -695,3 +832,38 @@ func TestOperation_IncludeTransaction(t *testing.T) {
tt.Assert.NotNil(op.Transaction)
tt.Assert.Equal(op.TransactionHash, op.Transaction.ID)
}

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 @@ -673,6 +673,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}})

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

0 comments on commit 380355e

Please sign in to comment.