From 9843bb0cee8360a5d1cb9f448f279403f8e604a5 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 8 Mar 2023 17:16:12 -0800 Subject: [PATCH 01/12] Move contract event parsing to its own package --- {ingest => contractevents}/event.go | 112 +++-------------------- {ingest => contractevents}/event_test.go | 2 +- contractevents/mint.go | 63 +++++++++++++ contractevents/transfer.go | 63 +++++++++++++ contractevents/utils.go | 39 ++++++++ 5 files changed, 180 insertions(+), 99 deletions(-) rename {ingest => contractevents}/event.go (62%) rename {ingest => contractevents}/event_test.go (99%) create mode 100644 contractevents/mint.go create mode 100644 contractevents/transfer.go create mode 100644 contractevents/utils.go diff --git a/ingest/event.go b/contractevents/event.go similarity index 62% rename from ingest/event.go rename to contractevents/event.go index 7a0eb9b676..29960e759b 100644 --- a/ingest/event.go +++ b/contractevents/event.go @@ -1,9 +1,6 @@ -package ingest +package contractevents import ( - "fmt" - - "github.com/stellar/go/strkey" "github.com/stellar/go/support/errors" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" @@ -18,12 +15,13 @@ type EventType = int const ( // Implemented EventTypeTransfer = iota + EventTypeMint = iota + // TODO: Not implemented EventTypeIncrAllow = iota EventTypeDecrAllow = iota EventTypeSetAuth = iota EventTypeSetAdmin = iota - EventTypeMint = iota EventTypeClawback = iota EventTypeBurn = iota ) @@ -39,7 +37,6 @@ var ( // TODO: Better parsing errors ErrNotStellarAssetContract = errors.New("event was not from a Stellar Asset Contract") ErrEventUnsupported = errors.New("this type of Stellar Asset Contract event is unsupported") - ErrNotTransferEvent = errors.New("event is an invalid 'transfer' event") ) type StellarAssetContractEvent interface { @@ -60,14 +57,6 @@ func (e *sacEvent) GetType() int { return e.Type } -type TransferEvent struct { - sacEvent - - From string - To string - Amount *xdr.Int128Parts -} - func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (StellarAssetContractEvent, error) { evt := &sacEvent{} @@ -141,8 +130,6 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell // This is the DEFINITIVE integrity check for whether or not this is a // SAC event. At this point, we can parse the event and treat it as // truth, mapping it to effects where appropriate. - fmt.Println(asset.IsNative(), "here?") - if expectedId != *event.ContractId { // nil check was earlier return evt, ErrNotStellarAssetContract } @@ -153,92 +140,21 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell return &xferEvent, xferEvent.parse(topics, value) case EventTypeMint: + mintEvent := MintEvent{} + return &mintEvent, mintEvent.parse(topics, value) + case EventTypeClawback: + fallthrough + // cbEvent := ClawbackEvent{} + // return &cbEvent, cbEvent.parse(topics, value) + case EventTypeBurn: + fallthrough + // burnEvent := BurnEvent{} + // return &burnEvent, burnEvent.parse(topics, value) + default: return evt, errors.Wrapf(ErrEventUnsupported, "event type %d ('%s') unsupported", evt.Type, fn.MustSym()) } - - return evt, nil -} - -// parseTransferEvent tries to parse the given topics and value as a SAC -// "transfer" event. It assumes that the `topics` array has already validated -// both the function name AND the asset <--> contract ID relationship. It will -// return a best-effort parsing even in error cases. -func (event *TransferEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { - // - // The transfer event format is: - // - // "transfer" Symbol - // Address - // Address - // Bytes - // - // i128 - // - if len(topics) != 4 { - return ErrNotTransferEvent - } - - from, to := topics[1], topics[2] - if from.Type != xdr.ScValTypeScvObject || to.Type != xdr.ScValTypeScvObject { - return ErrNotTransferEvent - } - - fromObj, ok := from.GetObj() - if !ok || fromObj == nil || fromObj.Type != xdr.ScObjectTypeScoAddress { - return ErrNotTransferEvent - } - - toObj, ok := from.GetObj() - if !ok || toObj == nil || toObj.Type != xdr.ScObjectTypeScoAddress { - return ErrNotTransferEvent - } - - event.From = ScAddressToString(fromObj.Address) - event.To = ScAddressToString(toObj.Address) - event.Asset = xdr.Asset{} // TODO - - valueObj, ok := value.GetObj() - if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { - return ErrNotTransferEvent - } - - event.Amount = valueObj.I128 - return nil -} - -// ScAddressToString converts the low-level `xdr.ScAddress` union into the -// appropriate strkey (contract C... or account ID G...). -// -// TODO: Should this return errors or just panic? Maybe just slap the "Must" -// prefix on the helper name? -func ScAddressToString(address *xdr.ScAddress) string { - if address == nil { - return "" - } - - var result string - var err error - - switch address.Type { - case xdr.ScAddressTypeScAddressTypeAccount: - pubkey := address.MustAccountId().Ed25519 - fmt.Println("pubkey:", address.MustAccountId()) - - result, err = strkey.Encode(strkey.VersionByteAccountID, pubkey[:]) - case xdr.ScAddressTypeScAddressTypeContract: - contractId := *address.ContractId - result, err = strkey.Encode(strkey.VersionByteContract, contractId[:]) - default: - panic(fmt.Errorf("unfamiliar address type: %v", address.Type)) - } - - if err != nil { - panic(err) - } - - return result } diff --git a/ingest/event_test.go b/contractevents/event_test.go similarity index 99% rename from ingest/event_test.go rename to contractevents/event_test.go index bf7dcca707..1d73312748 100644 --- a/ingest/event_test.go +++ b/contractevents/event_test.go @@ -1,4 +1,4 @@ -package ingest +package contractevents import ( "crypto/rand" diff --git a/contractevents/mint.go b/contractevents/mint.go new file mode 100644 index 0000000000..8e8971f4e1 --- /dev/null +++ b/contractevents/mint.go @@ -0,0 +1,63 @@ +package contractevents + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +var ErrNotMintEvent = errors.New("event is an invalid 'mint' event") + +type MintEvent struct { + sacEvent + + Admin string + To string + Amount xdr.Int128Parts +} + +// parseTransferEvent tries to parse the given topics and value as a SAC +// "transfer" event. It assumes that the `topics` array has already validated +// both the function name AND the asset <--> contract ID relationship. It will +// return a best-effort parsing even in error cases. +func (event *MintEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { + // + // The mint event format is: + // + // "mint" Symbol + // Address + // Address + // Bytes + // + // i128 + // + if len(topics) != 4 { + return ErrNotTransferEvent + } + + admin, to := topics[1], topics[2] + if admin.Type != xdr.ScValTypeScvObject || to.Type != xdr.ScValTypeScvObject { + return ErrNotMintEvent + } + + adminObj, ok := admin.GetObj() + if !ok || adminObj == nil || adminObj.Type != xdr.ScObjectTypeScoAddress { + return ErrNotMintEvent + } + + toObj, ok := to.GetObj() + if !ok || toObj == nil || toObj.Type != xdr.ScObjectTypeScoAddress { + return ErrNotMintEvent + } + + event.Admin = ScAddressToString(adminObj.Address) + event.To = ScAddressToString(toObj.Address) + event.Asset = xdr.Asset{} // TODO + + valueObj, ok := value.GetObj() + if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + return ErrNotMintEvent + } + + event.Amount = *valueObj.I128 + return nil +} diff --git a/contractevents/transfer.go b/contractevents/transfer.go new file mode 100644 index 0000000000..306f895977 --- /dev/null +++ b/contractevents/transfer.go @@ -0,0 +1,63 @@ +package contractevents + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +var ErrNotTransferEvent = errors.New("event is an invalid 'transfer' event") + +type TransferEvent struct { + sacEvent + + From string + To string + Amount xdr.Int128Parts +} + +// parseTransferEvent tries to parse the given topics and value as a SAC +// "transfer" event. It assumes that the `topics` array has already validated +// both the function name AND the asset <--> contract ID relationship. It will +// return a best-effort parsing even in error cases. +func (event *TransferEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { + // + // The transfer event format is: + // + // "transfer" Symbol + // Address + // Address + // Bytes + // + // i128 + // + if len(topics) != 4 { + return ErrNotTransferEvent + } + + from, to := topics[1], topics[2] + if from.Type != xdr.ScValTypeScvObject || to.Type != xdr.ScValTypeScvObject { + return ErrNotTransferEvent + } + + fromObj, ok := from.GetObj() + if !ok || fromObj == nil || fromObj.Type != xdr.ScObjectTypeScoAddress { + return ErrNotTransferEvent + } + + toObj, ok := to.GetObj() + if !ok || toObj == nil || toObj.Type != xdr.ScObjectTypeScoAddress { + return ErrNotTransferEvent + } + + event.From = ScAddressToString(fromObj.Address) + event.To = ScAddressToString(toObj.Address) + event.Asset = xdr.Asset{} // TODO + + valueObj, ok := value.GetObj() + if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + return ErrNotTransferEvent + } + + event.Amount = *valueObj.I128 + return nil +} diff --git a/contractevents/utils.go b/contractevents/utils.go new file mode 100644 index 0000000000..a38ed0e3a2 --- /dev/null +++ b/contractevents/utils.go @@ -0,0 +1,39 @@ +package contractevents + +import ( + "fmt" + + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +// ScAddressToString converts the low-level `xdr.ScAddress` union into the +// appropriate strkey (contract C... or account ID G...). +// +// TODO: Should this return errors or just panic? Maybe just slap the "Must" +// prefix on the helper name? +func ScAddressToString(address *xdr.ScAddress) string { + if address == nil { + return "" + } + + var result string + var err error + + switch address.Type { + case xdr.ScAddressTypeScAddressTypeAccount: + pubkey := address.MustAccountId().Ed25519 + result, err = strkey.Encode(strkey.VersionByteAccountID, pubkey[:]) + case xdr.ScAddressTypeScAddressTypeContract: + contractId := *address.ContractId + result, err = strkey.Encode(strkey.VersionByteContract, contractId[:]) + default: + panic(fmt.Errorf("unfamiliar address type: %v", address.Type)) + } + + if err != nil { + panic(err) + } + + return result +} From cefeb9261d2f8a61d43d802300450c82b3772b5c Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 8 Mar 2023 17:20:56 -0800 Subject: [PATCH 02/12] Add helper to parse ScAddresses --- contractevents/mint.go | 19 +++++-------------- contractevents/transfer.go | 19 +++++-------------- contractevents/utils.go | 13 +++++++++++++ 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/contractevents/mint.go b/contractevents/mint.go index 8e8971f4e1..376b47e835 100644 --- a/contractevents/mint.go +++ b/contractevents/mint.go @@ -34,23 +34,14 @@ func (event *MintEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { return ErrNotTransferEvent } - admin, to := topics[1], topics[2] - if admin.Type != xdr.ScValTypeScvObject || to.Type != xdr.ScValTypeScvObject { + rawAdmin, rawTo := topics[1], topics[2] + admin, to := parseAddress(&rawAdmin), parseAddress(&rawTo) + if admin == nil || to == nil { return ErrNotMintEvent } - adminObj, ok := admin.GetObj() - if !ok || adminObj == nil || adminObj.Type != xdr.ScObjectTypeScoAddress { - return ErrNotMintEvent - } - - toObj, ok := to.GetObj() - if !ok || toObj == nil || toObj.Type != xdr.ScObjectTypeScoAddress { - return ErrNotMintEvent - } - - event.Admin = ScAddressToString(adminObj.Address) - event.To = ScAddressToString(toObj.Address) + event.Admin = ScAddressToString(admin) + event.To = ScAddressToString(to) event.Asset = xdr.Asset{} // TODO valueObj, ok := value.GetObj() diff --git a/contractevents/transfer.go b/contractevents/transfer.go index 306f895977..30e0d20b22 100644 --- a/contractevents/transfer.go +++ b/contractevents/transfer.go @@ -34,23 +34,14 @@ func (event *TransferEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { return ErrNotTransferEvent } - from, to := topics[1], topics[2] - if from.Type != xdr.ScValTypeScvObject || to.Type != xdr.ScValTypeScvObject { + rawFrom, rawTo := topics[1], topics[2] + from, to := parseAddress(&rawFrom), parseAddress(&rawTo) + if from == nil || to == nil { return ErrNotTransferEvent } - fromObj, ok := from.GetObj() - if !ok || fromObj == nil || fromObj.Type != xdr.ScObjectTypeScoAddress { - return ErrNotTransferEvent - } - - toObj, ok := to.GetObj() - if !ok || toObj == nil || toObj.Type != xdr.ScObjectTypeScoAddress { - return ErrNotTransferEvent - } - - event.From = ScAddressToString(fromObj.Address) - event.To = ScAddressToString(toObj.Address) + event.From = ScAddressToString(from) + event.To = ScAddressToString(to) event.Asset = xdr.Asset{} // TODO valueObj, ok := value.GetObj() diff --git a/contractevents/utils.go b/contractevents/utils.go index a38ed0e3a2..4c0f4f6ea3 100644 --- a/contractevents/utils.go +++ b/contractevents/utils.go @@ -37,3 +37,16 @@ func ScAddressToString(address *xdr.ScAddress) string { return result } + +func parseAddress(val *xdr.ScVal) *xdr.ScAddress { + if val == nil { + return nil + } + + address, ok := val.GetObj() + if !ok || address == nil || address.Type != xdr.ScObjectTypeScoAddress { + return nil + } + + return address.Address +} From 88086e1e65b44f91a19bdd99dd9dfd2718a9d331 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 8 Mar 2023 17:38:44 -0800 Subject: [PATCH 03/12] Clean up enum usage --- contractevents/event.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contractevents/event.go b/contractevents/event.go index 29960e759b..e3f5a1fb1b 100644 --- a/contractevents/event.go +++ b/contractevents/event.go @@ -7,23 +7,22 @@ import ( ) type Event = xdr.ContractEvent -type EventType = int +type EventType int // Note that there is no distinction between xfer() and xfer_from() in events, // nor the other *_from variants. This is intentional from the host environment. const ( // Implemented - EventTypeTransfer = iota - EventTypeMint = iota - + EventTypeTransfer EventType = iota + EventTypeMint // TODO: Not implemented - EventTypeIncrAllow = iota - EventTypeDecrAllow = iota - EventTypeSetAuth = iota - EventTypeSetAdmin = iota - EventTypeClawback = iota - EventTypeBurn = iota + EventTypeIncrAllow + EventTypeDecrAllow + EventTypeSetAuth + EventTypeSetAdmin + EventTypeClawback + EventTypeBurn ) var ( @@ -40,12 +39,12 @@ var ( ) type StellarAssetContractEvent interface { - GetType() int + GetType() EventType GetAsset() xdr.Asset } type sacEvent struct { - Type int + Type EventType Asset xdr.Asset } @@ -53,7 +52,7 @@ func (e *sacEvent) GetAsset() xdr.Asset { return e.Asset } -func (e *sacEvent) GetType() int { +func (e *sacEvent) GetType() EventType { return e.Type } @@ -81,7 +80,8 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell if fn.Type != xdr.ScValTypeScvSymbol { return evt, ErrNotStellarAssetContract } - if eventType, ok := STELLAR_ASSET_CONTRACT_TOPICS[fn.MustSym()]; !ok { + + if eventType, ok := STELLAR_ASSET_CONTRACT_TOPICS[*fn.Sym]; !ok { return evt, ErrNotStellarAssetContract } else { evt.Type = eventType From b680fca80798d6262fe22639e376f0a30a2617b4 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 8 Mar 2023 17:39:15 -0800 Subject: [PATCH 04/12] Add mint test cases, contract address parsing --- contractevents/event.go | 6 +- contractevents/event_test.go | 167 ++++++++++++++++++++++++----------- 2 files changed, 119 insertions(+), 54 deletions(-) diff --git a/contractevents/event.go b/contractevents/event.go index e3f5a1fb1b..4fa2b7526c 100644 --- a/contractevents/event.go +++ b/contractevents/event.go @@ -27,8 +27,8 @@ const ( var ( STELLAR_ASSET_CONTRACT_TOPICS = map[xdr.ScSymbol]EventType{ - xdr.ScSymbol("mint"): EventTypeMint, xdr.ScSymbol("transfer"): EventTypeTransfer, + xdr.ScSymbol("mint"): EventTypeMint, xdr.ScSymbol("clawback"): EventTypeClawback, xdr.ScSymbol("burn"): EventTypeBurn, } @@ -136,11 +136,11 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell switch evt.GetType() { case EventTypeTransfer: - xferEvent := TransferEvent{} + xferEvent := TransferEvent{sacEvent: *evt} return &xferEvent, xferEvent.parse(topics, value) case EventTypeMint: - mintEvent := MintEvent{} + mintEvent := MintEvent{sacEvent: *evt} return &mintEvent, mintEvent.parse(topics, value) case EventTypeClawback: diff --git a/contractevents/event_test.go b/contractevents/event_test.go index 1d73312748..a977c60e1d 100644 --- a/contractevents/event_test.go +++ b/contractevents/event_test.go @@ -2,10 +2,10 @@ package contractevents import ( "crypto/rand" - "fmt" "testing" "github.com/stellar/go/keypair" + "github.com/stellar/go/strkey" "github.com/stellar/go/xdr" "github.com/stretchr/testify/require" @@ -13,33 +13,29 @@ import ( const passphrase = "passphrase" -func TestSACTransferEvent(t *testing.T) { - randomIssuer := keypair.MustRandom() - randomAsset := xdr.MustNewCreditAsset("TESTING", randomIssuer.Address()) - randomAccount := keypair.MustRandom().Address() +var ( + randomIssuer = keypair.MustRandom() + randomAsset = xdr.MustNewCreditAsset("TESTING", randomIssuer.Address()) + randomAccount = keypair.MustRandom().Address() + zeroContractHash = xdr.Hash([32]byte{}) + zeroContract = strkey.MustEncode(strkey.VersionByteContract, zeroContractHash[:]) +) +func TestSACTransferEvent(t *testing.T) { rawNativeContractId, err := xdr.MustNewNativeAsset().ContractID(passphrase) require.NoError(t, err) - rawContractId, err := randomAsset.ContractID(passphrase) - require.NoError(t, err) - nativeContractId := xdr.Hash(rawNativeContractId) - contractId := xdr.Hash(rawContractId) - baseXdrEvent := xdr.ContractEvent{ - Ext: xdr.ExtensionPoint{V: 0}, - ContractId: &contractId, - Type: xdr.ContractEventTypeContract, - Body: xdr.ContractEventBody{ - V: 0, - V0: nil, - }, - } + var baseXdrEvent xdr.ContractEvent + resetEvent := func() { + baseXdrEvent = makeEvent() + baseXdrEvent.Body.V0 = &xdr.ContractEventV0{ + Topics: makeTransferTopic(randomAsset), + Data: makeAmount(10000), + } - baseXdrEvent.Body.V0 = &xdr.ContractEventV0{ - Topics: makeTransferTopic(randomAsset, randomAccount), - Data: makeAmount(10000), } + resetEvent() // Ensure the happy path for transfer events works sacEvent, err := NewStellarAssetContractEvent(&baseXdrEvent, passphrase) @@ -49,51 +45,105 @@ func TestSACTransferEvent(t *testing.T) { xferEvent := sacEvent.(*TransferEvent) require.Equal(t, randomAccount, xferEvent.From) - require.Equal(t, randomAccount, xferEvent.To) + require.Equal(t, zeroContract, xferEvent.To) require.EqualValues(t, 10000, xferEvent.Amount.Lo) require.EqualValues(t, 0, xferEvent.Amount.Hi) // Ensure that changing the passphrase invalidates the event - _, err = NewStellarAssetContractEvent(&baseXdrEvent, "different") - require.Error(t, err) + t.Run("wrong passphrase", func(t *testing.T) { + _, err = NewStellarAssetContractEvent(&baseXdrEvent, "different") + require.Error(t, err) + }) - // Ensure that it works for the native asset - baseXdrEvent.ContractId = &nativeContractId - baseXdrEvent.Body.V0.Topics = makeTransferTopic(xdr.MustNewNativeAsset(), randomAccount) - sacEvent, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) - require.NoError(t, err) - require.Equal(t, xdr.AssetTypeAssetTypeNative, sacEvent.GetAsset().Type) + // Ensure that the native asset still works + t.Run("native transfer", func(t *testing.T) { + resetEvent() + baseXdrEvent.ContractId = &nativeContractId + baseXdrEvent.Body.V0.Topics = makeTransferTopic(xdr.MustNewNativeAsset()) + sacEvent, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.NoError(t, err) + require.Equal(t, xdr.AssetTypeAssetTypeNative, sacEvent.GetAsset().Type) + }) // Ensure that invalid asset binaries are rejected - bsAsset := make([]byte, 42) - rand.Read(bsAsset) - (*baseXdrEvent.Body.V0.Topics[3].Obj).Bin = &bsAsset - _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) - require.Error(t, err) + t.Run("bad asset binary", func(t *testing.T) { + resetEvent() + bsAsset := make([]byte, 42) + rand.Read(bsAsset) + (*baseXdrEvent.Body.V0.Topics[3].Obj).Bin = &bsAsset + _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.Error(t, err) + }) // Ensure that valid asset binaries that mismatch the contract are rejected - baseXdrEvent.ContractId = &nativeContractId - baseXdrEvent.Body.V0.Topics = makeTransferTopic(randomAsset, randomAccount) - _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) - require.Error(t, err) - baseXdrEvent.ContractId = &contractId - _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + t.Run("mismatching ID", func(t *testing.T) { + resetEvent() + // change the ID but keep the asset + baseXdrEvent.ContractId = &nativeContractId + _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.Error(t, err) + + // now change the asset but keep the ID + resetEvent() + diffRandomAsset := xdr.MustNewCreditAsset("TESTING", keypair.MustRandom().Address()) + baseXdrEvent.Body.V0.Topics = makeTransferTopic(diffRandomAsset) + _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.Error(t, err) + }) + + // Ensure that system events are rejected + t.Run("system events", func(t *testing.T) { + resetEvent() + baseXdrEvent.Type = xdr.ContractEventTypeSystem + _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.Error(t, err) + baseXdrEvent.Type = xdr.ContractEventTypeContract + }) +} + +func TestSACMintEvent(t *testing.T) { + baseXdrEvent := makeEvent() + baseXdrEvent.Body.V0 = &xdr.ContractEventV0{ + Topics: makeMintTopic(randomAsset), + Data: makeAmount(10000), + } + + // Ensure the happy path for mint events works + sacEvent, err := NewStellarAssetContractEvent(&baseXdrEvent, passphrase) require.NoError(t, err) + require.NotNil(t, sacEvent) + require.Equal(t, EventTypeMint, sacEvent.GetType()) - // Ensure that system events are invalid - baseXdrEvent.Type = xdr.ContractEventTypeSystem - _, err = NewStellarAssetContractEvent(&baseXdrEvent, passphrase) - require.Error(t, err) - baseXdrEvent.Type = xdr.ContractEventTypeContract + mintEvent := sacEvent.(*MintEvent) + require.Equal(t, randomAccount, mintEvent.Admin) + require.Equal(t, zeroContract, mintEvent.To) + require.EqualValues(t, 10000, mintEvent.Amount.Lo) + require.EqualValues(t, 0, mintEvent.Amount.Hi) } -func makeTransferTopic(asset xdr.Asset, participant string) xdr.ScVec { - accountId, err := xdr.AddressToAccountId(participant) +func makeEvent() xdr.ContractEvent { + rawContractId, err := randomAsset.ContractID(passphrase) if err != nil { - panic(fmt.Errorf("participant (%s) isn't an account ID: %v", - participant, err)) + panic(err) + } + contractId := xdr.Hash(rawContractId) + + baseXdrEvent := xdr.ContractEvent{ + Ext: xdr.ExtensionPoint{V: 0}, + ContractId: &contractId, + Type: xdr.ContractEventTypeContract, + Body: xdr.ContractEventBody{ + V: 0, + V0: &xdr.ContractEventV0{}, + }, } + return baseXdrEvent +} + +func makeTransferTopic(asset xdr.Asset) xdr.ScVec { + accountId := xdr.MustAddress(randomAccount) + fnName := xdr.ScSymbol("transfer") account := &xdr.ScObject{ Type: xdr.ScObjectTypeScoAddress, @@ -102,6 +152,13 @@ func makeTransferTopic(asset xdr.Asset, participant string) xdr.ScVec { AccountId: &accountId, }, } + contract := &xdr.ScObject{ + Type: xdr.ScObjectTypeScoAddress, + Address: &xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &zeroContractHash, + }, + } slice := []byte("native") if asset.Type != xdr.AssetTypeAssetTypeNative { @@ -126,7 +183,7 @@ func makeTransferTopic(asset xdr.Asset, participant string) xdr.ScVec { // to { Type: xdr.ScValTypeScvObject, - Obj: &account, + Obj: &contract, }, // asset details { @@ -136,6 +193,14 @@ func makeTransferTopic(asset xdr.Asset, participant string) xdr.ScVec { }) } +func makeMintTopic(asset xdr.Asset) xdr.ScVec { + // mint is just transfer but with an admin instead of a from... nice + fnName := xdr.ScSymbol("mint") + topics := makeTransferTopic(asset) + topics[0].Sym = &fnName + return topics +} + func makeAmount(amount int) xdr.ScVal { amountObj := &xdr.ScObject{ Type: xdr.ScObjectTypeScoI128, From 5b9206792213f8771328a768d48dc62e024c13a7 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 8 Mar 2023 18:02:02 -0800 Subject: [PATCH 05/12] Add fuzzing to parser to ensure no panics --- contractevents/event_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contractevents/event_test.go b/contractevents/event_test.go index a977c60e1d..cbd2417a1e 100644 --- a/contractevents/event_test.go +++ b/contractevents/event_test.go @@ -4,10 +4,13 @@ import ( "crypto/rand" "testing" + "github.com/stellar/go/gxdr" "github.com/stellar/go/keypair" + "github.com/stellar/go/randxdr" "github.com/stellar/go/strkey" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -121,6 +124,22 @@ func TestSACMintEvent(t *testing.T) { require.EqualValues(t, 0, mintEvent.Amount.Hi) } +func TestFuzzingSACEventParser(t *testing.T) { + gen := randxdr.NewGenerator() + for i := 0; i < 100_000; i++ { + event, shape := xdr.ContractEvent{}, &gxdr.ContractEvent{} + + gen.Next( + shape, + []randxdr.Preset{}, + ) + assert.NoError(t, gxdr.Convert(shape, &event)) + + // return values are ignored, but this should never panic + NewStellarAssetContractEvent(&event, "passphrase") + } +} + func makeEvent() xdr.ContractEvent { rawContractId, err := randomAsset.ContractID(passphrase) if err != nil { From c125d33206de5e57ac884bca7236ee506ca04a5d Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 8 Mar 2023 18:03:14 -0800 Subject: [PATCH 06/12] Move under support/ directory, seems ok? --- {contractevents => support/contractevents}/event.go | 0 {contractevents => support/contractevents}/event_test.go | 0 {contractevents => support/contractevents}/mint.go | 0 {contractevents => support/contractevents}/transfer.go | 0 {contractevents => support/contractevents}/utils.go | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {contractevents => support/contractevents}/event.go (100%) rename {contractevents => support/contractevents}/event_test.go (100%) rename {contractevents => support/contractevents}/mint.go (100%) rename {contractevents => support/contractevents}/transfer.go (100%) rename {contractevents => support/contractevents}/utils.go (100%) diff --git a/contractevents/event.go b/support/contractevents/event.go similarity index 100% rename from contractevents/event.go rename to support/contractevents/event.go diff --git a/contractevents/event_test.go b/support/contractevents/event_test.go similarity index 100% rename from contractevents/event_test.go rename to support/contractevents/event_test.go diff --git a/contractevents/mint.go b/support/contractevents/mint.go similarity index 100% rename from contractevents/mint.go rename to support/contractevents/mint.go diff --git a/contractevents/transfer.go b/support/contractevents/transfer.go similarity index 100% rename from contractevents/transfer.go rename to support/contractevents/transfer.go diff --git a/contractevents/utils.go b/support/contractevents/utils.go similarity index 100% rename from contractevents/utils.go rename to support/contractevents/utils.go From d432970d90e72c92b34ae982c3f8419133cbde02 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 9 Mar 2023 09:18:16 -0800 Subject: [PATCH 07/12] Bit of cleanup after PR comments --- support/contractevents/mint.go | 7 +++---- support/contractevents/transfer.go | 7 +++---- support/contractevents/utils.go | 7 ++----- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/support/contractevents/mint.go b/support/contractevents/mint.go index 376b47e835..24a931097d 100644 --- a/support/contractevents/mint.go +++ b/support/contractevents/mint.go @@ -5,7 +5,7 @@ import ( "github.com/stellar/go/xdr" ) -var ErrNotMintEvent = errors.New("event is an invalid 'mint' event") +var ErrNotMintEvent = errors.New("event is not a valid 'mint' event") type MintEvent struct { sacEvent @@ -40,9 +40,8 @@ func (event *MintEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { return ErrNotMintEvent } - event.Admin = ScAddressToString(admin) - event.To = ScAddressToString(to) - event.Asset = xdr.Asset{} // TODO + event.Admin = MustScAddressToString(admin) + event.To = MustScAddressToString(to) valueObj, ok := value.GetObj() if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { diff --git a/support/contractevents/transfer.go b/support/contractevents/transfer.go index 30e0d20b22..3cbdb4ac58 100644 --- a/support/contractevents/transfer.go +++ b/support/contractevents/transfer.go @@ -5,7 +5,7 @@ import ( "github.com/stellar/go/xdr" ) -var ErrNotTransferEvent = errors.New("event is an invalid 'transfer' event") +var ErrNotTransferEvent = errors.New("event is not a valid 'transfer' event") type TransferEvent struct { sacEvent @@ -40,9 +40,8 @@ func (event *TransferEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { return ErrNotTransferEvent } - event.From = ScAddressToString(from) - event.To = ScAddressToString(to) - event.Asset = xdr.Asset{} // TODO + event.From = MustScAddressToString(from) + event.To = MustScAddressToString(to) valueObj, ok := value.GetObj() if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { diff --git a/support/contractevents/utils.go b/support/contractevents/utils.go index 4c0f4f6ea3..8f964b4785 100644 --- a/support/contractevents/utils.go +++ b/support/contractevents/utils.go @@ -7,12 +7,9 @@ import ( "github.com/stellar/go/xdr" ) -// ScAddressToString converts the low-level `xdr.ScAddress` union into the +// MustScAddressToString converts the low-level `xdr.ScAddress` union into the // appropriate strkey (contract C... or account ID G...). -// -// TODO: Should this return errors or just panic? Maybe just slap the "Must" -// prefix on the helper name? -func ScAddressToString(address *xdr.ScAddress) string { +func MustScAddressToString(address *xdr.ScAddress) string { if address == nil { return "" } From 9511644b7628869870cadacb883b1cc204cec39d Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 9 Mar 2023 09:31:46 -0800 Subject: [PATCH 08/12] Add clawback event parsing, abstract to generic parser --- support/contractevents/clawback.go | 41 ++++++++++++++++++++++++++++ support/contractevents/event.go | 5 ++-- support/contractevents/event_test.go | 32 ++++++++++++++++++++++ support/contractevents/mint.go | 30 ++++++-------------- support/contractevents/transfer.go | 28 ++++++------------- support/contractevents/utils.go | 32 ++++++++++++++++++++++ 6 files changed, 124 insertions(+), 44 deletions(-) create mode 100644 support/contractevents/clawback.go diff --git a/support/contractevents/clawback.go b/support/contractevents/clawback.go new file mode 100644 index 0000000000..847c619d88 --- /dev/null +++ b/support/contractevents/clawback.go @@ -0,0 +1,41 @@ +package contractevents + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +var ErrNotClawbackEvent = errors.New("event is not a valid 'clawback' event") + +type ClawbackEvent struct { + sacEvent + + Admin string + From string + Amount xdr.Int128Parts +} + +// parseClawbackEvent tries to parse the given topics and value as a SAC +// "clawback" event. +// +// Internally, it assumes that the `topics` array has already validated both the +// function name AND the asset <--> contract ID relationship. It will return a +// best-effort parsing even in error cases. +func (event *ClawbackEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { + // + // The clawback event format is: + // + // "clawback" Symbol + // Address + // Address + // Bytes + // + // i128 + // + var err error + event.Admin, event.From, event.Amount, err = parseBalanceChangeEvent(topics, value) + if err != nil { + return ErrNotClawbackEvent + } + return nil +} diff --git a/support/contractevents/event.go b/support/contractevents/event.go index 4fa2b7526c..b70c279e6c 100644 --- a/support/contractevents/event.go +++ b/support/contractevents/event.go @@ -144,9 +144,8 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell return &mintEvent, mintEvent.parse(topics, value) case EventTypeClawback: - fallthrough - // cbEvent := ClawbackEvent{} - // return &cbEvent, cbEvent.parse(topics, value) + cbEvent := ClawbackEvent{} + return &cbEvent, cbEvent.parse(topics, value) case EventTypeBurn: fallthrough diff --git a/support/contractevents/event_test.go b/support/contractevents/event_test.go index cbd2417a1e..c776527db3 100644 --- a/support/contractevents/event_test.go +++ b/support/contractevents/event_test.go @@ -124,6 +124,26 @@ func TestSACMintEvent(t *testing.T) { require.EqualValues(t, 0, mintEvent.Amount.Hi) } +func TestSACClawbackEvent(t *testing.T) { + baseXdrEvent := makeEvent() + baseXdrEvent.Body.V0 = &xdr.ContractEventV0{ + Topics: makeClawbackTopic(randomAsset), + Data: makeAmount(10000), + } + + // Ensure the happy path for mint events works + sacEvent, err := NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.NoError(t, err) + require.NotNil(t, sacEvent) + require.Equal(t, EventTypeClawback, sacEvent.GetType()) + + clawEvent := sacEvent.(*ClawbackEvent) + require.Equal(t, randomAccount, clawEvent.Admin) + require.Equal(t, zeroContract, clawEvent.From) + require.EqualValues(t, 10000, clawEvent.Amount.Lo) + require.EqualValues(t, 0, clawEvent.Amount.Hi) +} + func TestFuzzingSACEventParser(t *testing.T) { gen := randxdr.NewGenerator() for i := 0; i < 100_000; i++ { @@ -140,6 +160,10 @@ func TestFuzzingSACEventParser(t *testing.T) { } } +// +// Test suite helpers below +// + func makeEvent() xdr.ContractEvent { rawContractId, err := randomAsset.ContractID(passphrase) if err != nil { @@ -220,6 +244,14 @@ func makeMintTopic(asset xdr.Asset) xdr.ScVec { return topics } +func makeClawbackTopic(asset xdr.Asset) xdr.ScVec { + // clawback is just mint but with an from instead of to + fnName := xdr.ScSymbol("clawback") + topics := makeTransferTopic(asset) + topics[0].Sym = &fnName + return topics +} + func makeAmount(amount int) xdr.ScVal { amountObj := &xdr.ScObject{ Type: xdr.ScObjectTypeScoI128, diff --git a/support/contractevents/mint.go b/support/contractevents/mint.go index 24a931097d..a2c72be2fc 100644 --- a/support/contractevents/mint.go +++ b/support/contractevents/mint.go @@ -15,10 +15,12 @@ type MintEvent struct { Amount xdr.Int128Parts } -// parseTransferEvent tries to parse the given topics and value as a SAC -// "transfer" event. It assumes that the `topics` array has already validated -// both the function name AND the asset <--> contract ID relationship. It will -// return a best-effort parsing even in error cases. +// parseMintEvent tries to parse the given topics and value as a SAC "mint" +// event. +// +// Internally, it assumes that the `topics` array has already validated both the +// function name AND the asset <--> contract ID relationship. It will return a +// best-effort parsing even in error cases. func (event *MintEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { // // The mint event format is: @@ -30,24 +32,10 @@ func (event *MintEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { // // i128 // - if len(topics) != 4 { - return ErrNotTransferEvent - } - - rawAdmin, rawTo := topics[1], topics[2] - admin, to := parseAddress(&rawAdmin), parseAddress(&rawTo) - if admin == nil || to == nil { + var err error + event.Admin, event.To, event.Amount, err = parseBalanceChangeEvent(topics, value) + if err != nil { return ErrNotMintEvent } - - event.Admin = MustScAddressToString(admin) - event.To = MustScAddressToString(to) - - valueObj, ok := value.GetObj() - if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { - return ErrNotMintEvent - } - - event.Amount = *valueObj.I128 return nil } diff --git a/support/contractevents/transfer.go b/support/contractevents/transfer.go index 3cbdb4ac58..23c8896cc3 100644 --- a/support/contractevents/transfer.go +++ b/support/contractevents/transfer.go @@ -16,9 +16,11 @@ type TransferEvent struct { } // parseTransferEvent tries to parse the given topics and value as a SAC -// "transfer" event. It assumes that the `topics` array has already validated -// both the function name AND the asset <--> contract ID relationship. It will -// return a best-effort parsing even in error cases. +// "transfer" event. +// +// Internally, it assumes that the `topics` array has already validated both the +// function name AND the asset <--> contract ID relationship. It will return a +// best-effort parsing even in error cases. func (event *TransferEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { // // The transfer event format is: @@ -30,24 +32,10 @@ func (event *TransferEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { // // i128 // - if len(topics) != 4 { + var err error + event.From, event.To, event.Amount, err = parseBalanceChangeEvent(topics, value) + if err != nil { return ErrNotTransferEvent } - - rawFrom, rawTo := topics[1], topics[2] - from, to := parseAddress(&rawFrom), parseAddress(&rawTo) - if from == nil || to == nil { - return ErrNotTransferEvent - } - - event.From = MustScAddressToString(from) - event.To = MustScAddressToString(to) - - valueObj, ok := value.GetObj() - if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { - return ErrNotTransferEvent - } - - event.Amount = *valueObj.I128 return nil } diff --git a/support/contractevents/utils.go b/support/contractevents/utils.go index 8f964b4785..a427b3ebb7 100644 --- a/support/contractevents/utils.go +++ b/support/contractevents/utils.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stellar/go/strkey" + "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) @@ -47,3 +48,34 @@ func parseAddress(val *xdr.ScVal) *xdr.ScAddress { return address.Address } + +// parseBalanceChangeEvent is a generalization of a subset of the Stellar Asset +// Contract events. Transfer, mint, clawback, and burn events all have two +// addresses and an amount involved. The addresses represent different things in +// different event types (e.g. "from" or "admin"), but the parsing is identical. +// This helper extracts all three parts or returns a generic error if it can't. +var ErrNotBalanceChangeEvent = errors.New("") + +func parseBalanceChangeEvent(topics xdr.ScVec, value xdr.ScVal) (string, string, xdr.Int128Parts, error) { + first, second, amount := "", "", xdr.Int128Parts{} + + if len(topics) != 4 { + return first, second, amount, ErrNotBalanceChangeEvent + } + + rawFirst, rawSecond := topics[1], topics[2] + firstSc, secondSc := parseAddress(&rawFirst), parseAddress(&rawSecond) + if firstSc == nil || secondSc == nil { + return first, second, amount, ErrNotBalanceChangeEvent + } + + first, second = MustScAddressToString(firstSc), MustScAddressToString(secondSc) + + valueObj, ok := value.GetObj() + if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + return first, second, amount, ErrNotBalanceChangeEvent + } + + amount = *valueObj.I128 + return first, second, amount, nil +} From 46b2fc19b48fc70fff1054c62601a5c461ddf6f4 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 9 Mar 2023 09:40:38 -0800 Subject: [PATCH 09/12] Add burn event support --- support/contractevents/burn.go | 54 ++++++++++++++++++++++++++++ support/contractevents/event.go | 13 ++++--- support/contractevents/event_test.go | 32 +++++++++++++++-- support/contractevents/utils.go | 2 +- 4 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 support/contractevents/burn.go diff --git a/support/contractevents/burn.go b/support/contractevents/burn.go new file mode 100644 index 0000000000..eeba991262 --- /dev/null +++ b/support/contractevents/burn.go @@ -0,0 +1,54 @@ +package contractevents + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +var ErrNotBurnEvent = errors.New("event is not a valid 'mint' event") + +type BurnEvent struct { + sacEvent + + From string + Amount xdr.Int128Parts +} + +// parseBurnEvent tries to parse the given topics and value as a SAC "burn" +// event. +// +// Internally, it assumes that the `topics` array has already validated both the +// function name AND the asset <--> contract ID relationship. It will return a +// best-effort parsing even in error cases. +func (event *BurnEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { + // + // The burn event format is: + // + // "burn" Symbol + // Address + // Bytes + // + // i128 + // + // Reference: https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-host/src/native_contract/token/event.rs#L102-L109 + // + if len(topics) != 3 { + return ErrNotBurnEvent + } + + rawFrom := topics[1] + from := parseAddress(&rawFrom) + if from == nil { + return ErrNotBurnEvent + } + + event.From = MustScAddressToString(from) + + valueObj, ok := value.GetObj() + if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + return ErrNotBurnEvent + } + + event.Amount = *valueObj.I128 + return nil +} diff --git a/support/contractevents/event.go b/support/contractevents/event.go index b70c279e6c..2ed30d4142 100644 --- a/support/contractevents/event.go +++ b/support/contractevents/event.go @@ -16,13 +16,13 @@ const ( // Implemented EventTypeTransfer EventType = iota EventTypeMint + EventTypeClawback + EventTypeBurn // TODO: Not implemented EventTypeIncrAllow EventTypeDecrAllow EventTypeSetAuth EventTypeSetAdmin - EventTypeClawback - EventTypeBurn ) var ( @@ -33,7 +33,7 @@ var ( xdr.ScSymbol("burn"): EventTypeBurn, } - // TODO: Better parsing errors + // TODO: Finer-grained parsing errors ErrNotStellarAssetContract = errors.New("event was not from a Stellar Asset Contract") ErrEventUnsupported = errors.New("this type of Stellar Asset Contract event is unsupported") ) @@ -144,13 +144,12 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell return &mintEvent, mintEvent.parse(topics, value) case EventTypeClawback: - cbEvent := ClawbackEvent{} + cbEvent := ClawbackEvent{sacEvent: *evt} return &cbEvent, cbEvent.parse(topics, value) case EventTypeBurn: - fallthrough - // burnEvent := BurnEvent{} - // return &burnEvent, burnEvent.parse(topics, value) + burnEvent := BurnEvent{sacEvent: *evt} + return &burnEvent, burnEvent.parse(topics, value) default: return evt, errors.Wrapf(ErrEventUnsupported, diff --git a/support/contractevents/event_test.go b/support/contractevents/event_test.go index c776527db3..11e56368ed 100644 --- a/support/contractevents/event_test.go +++ b/support/contractevents/event_test.go @@ -131,7 +131,7 @@ func TestSACClawbackEvent(t *testing.T) { Data: makeAmount(10000), } - // Ensure the happy path for mint events works + // Ensure the happy path for clawback events works sacEvent, err := NewStellarAssetContractEvent(&baseXdrEvent, passphrase) require.NoError(t, err) require.NotNil(t, sacEvent) @@ -144,6 +144,25 @@ func TestSACClawbackEvent(t *testing.T) { require.EqualValues(t, 0, clawEvent.Amount.Hi) } +func TestSACBurnEvent(t *testing.T) { + baseXdrEvent := makeEvent() + baseXdrEvent.Body.V0 = &xdr.ContractEventV0{ + Topics: makeBurnTopic(randomAsset), + Data: makeAmount(10000), + } + + // Ensure the happy path for burn events works + sacEvent, err := NewStellarAssetContractEvent(&baseXdrEvent, passphrase) + require.NoError(t, err) + require.NotNil(t, sacEvent) + require.Equal(t, EventTypeBurn, sacEvent.GetType()) + + burnEvent := sacEvent.(*BurnEvent) + require.Equal(t, randomAccount, burnEvent.From) + require.EqualValues(t, 10000, burnEvent.Amount.Lo) + require.EqualValues(t, 0, burnEvent.Amount.Hi) +} + func TestFuzzingSACEventParser(t *testing.T) { gen := randxdr.NewGenerator() for i := 0; i < 100_000; i++ { @@ -245,13 +264,22 @@ func makeMintTopic(asset xdr.Asset) xdr.ScVec { } func makeClawbackTopic(asset xdr.Asset) xdr.ScVec { - // clawback is just mint but with an from instead of to + // clawback is just mint but with a from instead of a to fnName := xdr.ScSymbol("clawback") topics := makeTransferTopic(asset) topics[0].Sym = &fnName return topics } +func makeBurnTopic(asset xdr.Asset) xdr.ScVec { + // burn is like clawback but without a "to", so we drop that topic + fnName := xdr.ScSymbol("burn") + topics := makeTransferTopic(asset) + topics[0].Sym = &fnName + topics = append(topics[:2], topics[3:]...) + return topics +} + func makeAmount(amount int) xdr.ScVal { amountObj := &xdr.ScObject{ Type: xdr.ScObjectTypeScoI128, diff --git a/support/contractevents/utils.go b/support/contractevents/utils.go index a427b3ebb7..3bbcb875c1 100644 --- a/support/contractevents/utils.go +++ b/support/contractevents/utils.go @@ -54,7 +54,7 @@ func parseAddress(val *xdr.ScVal) *xdr.ScAddress { // addresses and an amount involved. The addresses represent different things in // different event types (e.g. "from" or "admin"), but the parsing is identical. // This helper extracts all three parts or returns a generic error if it can't. -var ErrNotBalanceChangeEvent = errors.New("") +var ErrNotBalanceChangeEvent = errors.New("event doesn't represent a balance change") func parseBalanceChangeEvent(topics xdr.ScVec, value xdr.ScVal) (string, string, xdr.Int128Parts, error) { first, second, amount := "", "", xdr.Int128Parts{} From 65aafb3d13809d745d77158994dc132fa455f53f Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 9 Mar 2023 09:58:45 -0800 Subject: [PATCH 10/12] Fixup linter complaint, my bad bro --- support/contractevents/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/support/contractevents/utils.go b/support/contractevents/utils.go index 3bbcb875c1..0e84857321 100644 --- a/support/contractevents/utils.go +++ b/support/contractevents/utils.go @@ -8,6 +8,8 @@ import ( "github.com/stellar/go/xdr" ) +var ErrNotBalanceChangeEvent = errors.New("event doesn't represent a balance change") + // MustScAddressToString converts the low-level `xdr.ScAddress` union into the // appropriate strkey (contract C... or account ID G...). func MustScAddressToString(address *xdr.ScAddress) string { @@ -54,8 +56,6 @@ func parseAddress(val *xdr.ScVal) *xdr.ScAddress { // addresses and an amount involved. The addresses represent different things in // different event types (e.g. "from" or "admin"), but the parsing is identical. // This helper extracts all three parts or returns a generic error if it can't. -var ErrNotBalanceChangeEvent = errors.New("event doesn't represent a balance change") - func parseBalanceChangeEvent(topics xdr.ScVec, value xdr.ScVal) (string, string, xdr.Int128Parts, error) { first, second, amount := "", "", xdr.Int128Parts{} From 2a667bb08f6a8d1a4a8e188164c29811e00b6988 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 9 Mar 2023 10:02:18 -0800 Subject: [PATCH 11/12] Add helper to parse i128 amounts --- support/contractevents/burn.go | 8 ++++---- support/contractevents/utils.go | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/support/contractevents/burn.go b/support/contractevents/burn.go index eeba991262..508f18bacb 100644 --- a/support/contractevents/burn.go +++ b/support/contractevents/burn.go @@ -5,7 +5,7 @@ import ( "github.com/stellar/go/xdr" ) -var ErrNotBurnEvent = errors.New("event is not a valid 'mint' event") +var ErrNotBurnEvent = errors.New("event is not a valid 'burn' event") type BurnEvent struct { sacEvent @@ -44,11 +44,11 @@ func (event *BurnEvent) parse(topics xdr.ScVec, value xdr.ScVal) error { event.From = MustScAddressToString(from) - valueObj, ok := value.GetObj() - if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + amount := parseAmount(&value) + if amount == nil { return ErrNotBurnEvent } - event.Amount = *valueObj.I128 + event.Amount = *amount return nil } diff --git a/support/contractevents/utils.go b/support/contractevents/utils.go index 0e84857321..99e8d8b3d0 100644 --- a/support/contractevents/utils.go +++ b/support/contractevents/utils.go @@ -51,6 +51,15 @@ func parseAddress(val *xdr.ScVal) *xdr.ScAddress { return address.Address } +func parseAmount(val *xdr.ScVal) *xdr.Int128Parts { + valueObj, ok := val.GetObj() + if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + return nil + } + + return valueObj.I128 +} + // parseBalanceChangeEvent is a generalization of a subset of the Stellar Asset // Contract events. Transfer, mint, clawback, and burn events all have two // addresses and an amount involved. The addresses represent different things in @@ -71,11 +80,11 @@ func parseBalanceChangeEvent(topics xdr.ScVec, value xdr.ScVal) (string, string, first, second = MustScAddressToString(firstSc), MustScAddressToString(secondSc) - valueObj, ok := value.GetObj() - if !ok || valueObj == nil || valueObj.Type != xdr.ScObjectTypeScoI128 { + amountPtr := parseAmount(&value) + if amountPtr == nil { return first, second, amount, ErrNotBalanceChangeEvent } - amount = *valueObj.I128 + amount = *amountPtr return first, second, amount, nil } From c597f6be80c21461ef0ed1333c929a55440e95d7 Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Thu, 9 Mar 2023 10:04:59 -0800 Subject: [PATCH 12/12] Simpler branching --- support/contractevents/event.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/support/contractevents/event.go b/support/contractevents/event.go index 2ed30d4142..8f8614fb64 100644 --- a/support/contractevents/event.go +++ b/support/contractevents/event.go @@ -112,14 +112,13 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell return evt, errors.Wrap(ErrNotStellarAssetContract, err.Error()) } - switch asset.IsNative() { - case true: - evt.Asset = xdr.MustNewNativeAsset() - case false: + if !asset.IsNative() { evt.Asset, err = xdr.NewCreditAsset(asset.GetCode(), asset.GetIssuer()) - } - if err != nil { - return evt, errors.Wrap(ErrNotStellarAssetContract, err.Error()) + if err != nil { + return evt, errors.Wrap(ErrNotStellarAssetContract, err.Error()) + } + } else { + evt.Asset = xdr.MustNewNativeAsset() } expectedId, err := evt.Asset.ContractID(networkPassphrase)