Skip to content

Commit

Permalink
stellar#4728: pr feedback, bring asset balance changes into InvokeFnO…
Browse files Browse the repository at this point in the history
…p details, include event type in op details
  • Loading branch information
sreuland committed Mar 17, 2023
1 parent c925b62 commit 1af0c9a
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 16 deletions.
24 changes: 21 additions & 3 deletions protocols/horizon/operations/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,13 @@ type LiquidityPoolWithdraw struct {
// 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
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 @@ -366,6 +368,22 @@ 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 was decremented
// To - this is the classic account that asset was incremented, or if not applicable
// for asset contract event, 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.
// The event is captured at ingestion time from the contract events present in tx meta.
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
70 changes: 68 additions & 2 deletions services/horizon/internal/actions/operation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/stellar/go/xdr"
)

func TestContractEventsInPaymentOperations(t *testing.T) {
func TestInvokeHostFnDetailsInPaymentOperations(t *testing.T) {
tt := test.Start(t)
defer tt.Finish()
test.ResetHorizonDB(t, tt.HorizonDB)
Expand Down Expand Up @@ -63,7 +63,44 @@ func TestContractEventsInPaymentOperations(t *testing.T) {
[]byte(`{
"parameters": [],
"function": "fn",
"footprint": ""
"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{},
Expand All @@ -81,6 +118,35 @@ func TestContractEventsInPaymentOperations(t *testing.T) {

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) {
Expand Down
47 changes: 36 additions & 11 deletions services/horizon/internal/ingest/processors/operations_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ func (operation *transactionOperationWrapper) Details() (map[string]interface{},
}

// Searches an operation for SAC events that are of a type which represent
// Asset balances changed between contracts and/or classic.
// Asset balances changed with at least one participant of the change referencing a classic account.
//
// SAC events have a one-to-one association to SAC contract fn invocations,
// i.e. invoke the 'mint' function, will trigger one Mint Event to be emitted capturing the fn args.
Expand All @@ -716,7 +716,6 @@ func (operation *transactionOperationWrapper) Details() (map[string]interface{},
// The 'amount' expressed as non-negative, the event type can provide the context of
// whether an amount was considered negative i.e. credit or debit to a balance.
//
// The 'from' and 'to' attributes represent an account or a contract
func (operation *transactionOperationWrapper) parseAssetBalanceChangesFromContractEvents() ([]map[string]interface{}, error) {
balanceChanges := []map[string]interface{}{}

Expand All @@ -728,22 +727,32 @@ func (operation *transactionOperationWrapper) parseAssetBalanceChangesFromContra
}

for _, contractEvent := range events {
// parse the xdr contract event to contractevents.StellarAssetContractEvent model
// has some convenience like to/from attributes are expressed in strkey format for contracts(C...) and accounts(G...)
// Parse the xdr contract event to contractevents.StellarAssetContractEvent model
// has some convenience like to/from attributes are expressed in strkey format for classic accounts(G...)
// For now, if the event refers to contract addresses only, then drop the event, don't
// include in the operation details, this may change if contract addresses become a recognized entity in horizon history model
if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, operation.networkPassphrase); err == nil {
switch sacEvent.GetType() {
case contractevents.EventTypeTransfer:
transferEvt := sacEvent.(contractevents.TransferEvent)
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(transferEvt.From, transferEvt.To, transferEvt.Amount, transferEvt.Asset))
if !contractevents.IsContractAddress(transferEvt.From) || !contractevents.IsContractAddress(transferEvt.To) {
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(transferEvt.From, transferEvt.To, transferEvt.Amount, transferEvt.Asset, "transfer"))
}
case contractevents.EventTypeMint:
mintEvt := sacEvent.(contractevents.MintEvent)
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(mintEvt.Admin, mintEvt.To, mintEvt.Amount, mintEvt.Asset))
if !contractevents.IsContractAddress(mintEvt.Admin) || !contractevents.IsContractAddress(mintEvt.To) {
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(mintEvt.Admin, mintEvt.To, mintEvt.Amount, mintEvt.Asset, "mint"))
}
case contractevents.EventTypeClawback:
clawbackEvt := sacEvent.(contractevents.ClawbackEvent)
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(clawbackEvt.From, clawbackEvt.Admin, clawbackEvt.Amount, clawbackEvt.Asset))
if !contractevents.IsContractAddress(clawbackEvt.From) || !contractevents.IsContractAddress(clawbackEvt.Admin) {
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(clawbackEvt.From, clawbackEvt.Admin, clawbackEvt.Amount, clawbackEvt.Asset, "clawback"))
}
case contractevents.EventTypeBurn:
burnEvt := sacEvent.(contractevents.BurnEvent)
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(burnEvt.From, "", burnEvt.Amount, burnEvt.Asset))
if !contractevents.IsContractAddress(burnEvt.From) {
balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(burnEvt.From, "", burnEvt.Amount, burnEvt.Asset, "burn"))
}
}
}
}
Expand All @@ -755,15 +764,31 @@ func (operation *transactionOperationWrapper) parseAssetBalanceChangesFromContra
// toAccount - strkey format of contract or address, or nillable
// amountChanged - absolute value that asset balance changed
// asset - the fully qualified issuer:code for asset that had balance change
// changeType - the type of source sac event that triggered this change
//
// return - a balance changed record expressed as map of key/value's
func createSACBalanceChangeEntry(fromAccount string, toAccount string, amountChanged xdr.Int128Parts, asset xdr.Asset) map[string]interface{} {
func createSACBalanceChangeEntry(fromAccount string, toAccount string, amountChanged xdr.Int128Parts, asset xdr.Asset, changeType string) map[string]interface{} {
balanceChange := map[string]interface{}{}

balanceChange["from"] = fromAccount
if contractevents.IsContractAddress(fromAccount) {
// TODO, place the 'C...' contract address here, if/when horizon has facility for
// contract addresses
balanceChange["from"] = "contract"
} else {
balanceChange["from"] = fromAccount
}

if toAccount != "" {
balanceChange["to"] = toAccount
if contractevents.IsContractAddress(toAccount) {
// TODO, place the 'C...' contract address here, if/when horizon has facility for
// contract addresses
balanceChange["to"] = "contract"
} else {
balanceChange["to"] = toAccount
}
}

balanceChange["type"] = changeType
balanceChange["amount"] = stringifyAmount(amountChanged)
addAssetDetails(balanceChange, asset, "")
return balanceChange
Expand Down
4 changes: 4 additions & 0 deletions support/contractevents/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ func MustScAddressToString(address *xdr.ScAddress) string {
return result
}

func IsContractAddress(account string) bool {
return account[0] == 'C'
}

func parseAddress(val *xdr.ScVal) *xdr.ScAddress {
if val == nil {
return nil
Expand Down

0 comments on commit 1af0c9a

Please sign in to comment.