Skip to content

Commit

Permalink
support/contractevents: Properly parse asset topic, add event generat…
Browse files Browse the repository at this point in the history
…or. (#4808)
  • Loading branch information
Shaptic authored Mar 15, 2023
1 parent c63bf26 commit f8ba8f1
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 88 deletions.
94 changes: 72 additions & 22 deletions support/contractevents/event.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package contractevents

import (
"bytes"
"fmt"

"github.com/stellar/go/support/errors"
"github.com/stellar/go/txnbuild"
"github.com/stellar/go/xdr"
)

Expand Down Expand Up @@ -36,6 +38,7 @@ var (
// 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")
ErrEventIntegrity = errors.New("contract ID doesn't match asset + passphrase")
)

type StellarAssetContractEvent interface {
Expand Down Expand Up @@ -67,21 +70,21 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell
//
// For specific event forms, see here:
// https://github.com/stellar/rs-soroban-env/blob/main/soroban-env-host/src/native_contract/token/event.rs#L44-L49
topics := event.Body.MustV0().Topics
value := event.Body.MustV0().Data
topics := event.Body.V0.Topics
value := event.Body.V0.Data

// No relevant SAC events have <= 2 topics
if len(topics) <= 2 {
return evt, ErrNotStellarAssetContract
}
fn := topics[0]

// Filter out events for function calls we don't care about
if fn.Type != xdr.ScValTypeScvSymbol {
fn, ok := topics[0].GetSym()
if !ok {
return evt, ErrNotStellarAssetContract
}

if eventType, ok := STELLAR_ASSET_CONTRACT_TOPICS[*fn.Sym]; !ok {
if eventType, found := STELLAR_ASSET_CONTRACT_TOPICS[fn]; !found {
return evt, ErrNotStellarAssetContract
} else {
evt.Type = eventType
Expand All @@ -91,11 +94,10 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell
//
// To check that, ensure that the contract ID of the event matches the
// contract ID that *would* represent the asset the event is claiming to
// be. The asset is in canonical SEP-11 form:
// https://stellar.org/protocol/sep-11#alphanum4-alphanum12
// be.
//
// For all parsing errors, we just continue, since it's not a real
// error, just an event non-complaint with SAC events.
// For all parsing errors, we just continue, since it's not a real error,
// just an event non-complaint with SAC events.
rawAsset := topics[len(topics)-1]
assetContainer, ok := rawAsset.GetObj()
if !ok || assetContainer == nil {
Expand All @@ -107,20 +109,12 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell
return evt, ErrNotStellarAssetContract
}

asset, err := txnbuild.ParseAssetString(string(assetBytes))
asset, err := parseAssetBytes(assetBytes)
if err != nil {
return evt, errors.Wrap(ErrNotStellarAssetContract, err.Error())
}

if !asset.IsNative() {
evt.Asset, err = xdr.NewCreditAsset(asset.GetCode(), asset.GetIssuer())
if err != nil {
return evt, errors.Wrap(ErrNotStellarAssetContract, err.Error())
}
} else {
evt.Asset = xdr.MustNewNativeAsset()
}

evt.Asset = *asset
expectedId, err := evt.Asset.ContractID(networkPassphrase)
if err != nil {
return evt, errors.Wrap(ErrNotStellarAssetContract, err.Error())
Expand All @@ -130,7 +124,7 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell
// SAC event. At this point, we can parse the event and treat it as
// truth, mapping it to effects where appropriate.
if expectedId != *event.ContractId { // nil check was earlier
return evt, ErrNotStellarAssetContract
return evt, ErrEventIntegrity
}

switch evt.GetType() {
Expand All @@ -152,6 +146,62 @@ func NewStellarAssetContractEvent(event *Event, networkPassphrase string) (Stell

default:
return evt, errors.Wrapf(ErrEventUnsupported,
"event type %d ('%s') unsupported", evt.Type, fn.MustSym())
"event type %d ('%s') unsupported", evt.Type, fn)
}
}

func parseAssetBytes(b []byte) (*xdr.Asset, error) {
// The asset is SORT OF in canonical SEP-11 form:
// https://stellar.org/protocol/sep-11#alphanum4-alphanum12
// namely, its split into code and issuer by a colon, but the issuer is a
// raw public key rather than strkey ascii bytes, and the code is padded to
// exactly 4 or 12 bytes.
asset := xdr.Asset{
Type: xdr.AssetTypeAssetTypeNative,
}

if string(b) == "native" {
return &asset, nil
}

parts := bytes.SplitN(b, []byte{':'}, 2)
if len(parts) != 2 {
return nil, errors.New("invalid asset byte format (expected <code>:<issuer>)")
}
rawCode, rawIssuerKey := parts[0], parts[1]

issuerKey := xdr.Uint256{}
if err := issuerKey.UnmarshalBinary(rawIssuerKey); err != nil {
return nil, errors.Wrap(err, "asset issuer not a public key")
}
accountId := xdr.AccountId(xdr.PublicKey{
Type: xdr.PublicKeyTypePublicKeyTypeEd25519,
Ed25519: &issuerKey,
})

if len(rawCode) == 4 {
code := [4]byte{}
copy(code[:], rawCode[:])

asset.Type = xdr.AssetTypeAssetTypeCreditAlphanum4
asset.AlphaNum4 = &xdr.AlphaNum4{
AssetCode: xdr.AssetCode4(code),
Issuer: accountId,
}
} else if len(rawCode) == 12 {
code := [12]byte{}
copy(code[:], rawCode[:])

asset.Type = xdr.AssetTypeAssetTypeCreditAlphanum12
asset.AlphaNum12 = &xdr.AlphaNum12{
AssetCode: xdr.AssetCode12(code),
Issuer: accountId,
}
} else {
return nil, fmt.Errorf(
"asset code invalid (expected 4 or 12 bytes, got %d: '%v' or '%s')",
len(rawCode), rawCode, string(rawCode))
}

return &asset, nil
}
150 changes: 89 additions & 61 deletions support/contractevents/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package contractevents

import (
"crypto/rand"
"encoding/base64"
"math"
"math/big"
"testing"

"github.com/stellar/go/gxdr"
Expand All @@ -24,6 +27,68 @@ var (
zeroContract = strkey.MustEncode(strkey.VersionByteContract, zeroContractHash[:])
)

func TestScValCreators(t *testing.T) {
val := makeSymbol("hello")
assert.Equal(t, val.Type, xdr.ScValTypeScvSymbol)
assert.NotNil(t, val.Sym)
assert.EqualValues(t, *val.Sym, "hello")

val = makeAmount(1234)
obj, ok := val.GetObj()
assert.True(t, ok)
assert.NotNil(t, obj)

amt, ok := obj.GetI128()
assert.True(t, ok)
assert.EqualValues(t, 0, amt.Hi)
assert.EqualValues(t, 1234, amt.Lo)

// make an amount which is 2^65 + 1234 to check both hi and lo parts
amount := big.NewInt(math.MaxInt64)
amount. // this is 2^63-1
Add(amount, big.NewInt(1)). // 2^63
Or(amount, big.NewInt(math.MaxInt64)). // 2^64-1 (max uint64)
Lsh(amount, 2). // now it's (2^64 - 1) * 4 = 2^66 - 4
Add(amount, big.NewInt(1234+4)) // now it's 2^66 + 1234

val = makeBigAmount(amount)
obj, ok = val.GetObj()
assert.True(t, ok)
assert.NotNil(t, obj)

amt, ok = obj.GetI128()
assert.True(t, ok)
assert.EqualValues(t, 4, amt.Hi)
assert.EqualValues(t, 1234, amt.Lo)
}

func TestEventGenerator(t *testing.T) {
passphrase := "This is a passphrase."
issuer := keypair.MustRandom().Address()
from, to, admin := issuer, issuer, issuer

for _, type_ := range []EventType{
EventTypeTransfer,
EventTypeMint,
EventTypeClawback,
EventTypeBurn,
} {
event := GenerateEvent(type_, from, to, admin, xdr.MustNewNativeAsset(), big.NewInt(12345), passphrase)
parsedEvent, err := NewStellarAssetContractEvent(&event, passphrase)
require.NoErrorf(t, err, "generating an event of type %v failed", type_)
require.Equal(t, type_, parsedEvent.GetType())
require.Equal(t, xdr.AssetTypeAssetTypeNative, parsedEvent.GetAsset().Type)

event = GenerateEvent(type_, from, to, admin,
xdr.MustNewCreditAsset("TESTER", issuer),
big.NewInt(12345), passphrase)
parsedEvent, err = NewStellarAssetContractEvent(&event, passphrase)
require.NoErrorf(t, err, "generating an event of type %v failed", type_)
require.Equal(t, type_, parsedEvent.GetType())
require.Equal(t, xdr.AssetTypeAssetTypeCreditAlphanum12, parsedEvent.GetAsset().Type)
}
}

func TestSACTransferEvent(t *testing.T) {
rawNativeContractId, err := xdr.MustNewNativeAsset().ContractID(passphrase)
require.NoError(t, err)
Expand Down Expand Up @@ -179,6 +244,20 @@ func TestFuzzingSACEventParser(t *testing.T) {
}
}

func TestRealXdr(t *testing.T) {
base64xdr := "AAAAAAAAAAGP097PJPXCcbtgOhu8wDc/ELPABxTdosN//YtrzxEJyAAAAAEAAAAAAAAABAAAAAUAAAAIdHJhbnNmZXIAAAAEAAAAAQAAAAgAAAAAAAAAAHN2/eiOTNYcwPspSheGs/HQYfXy8cpXRl+qkyIRuUbWAAAABAAAAAEAAAAIAAAAAAAAAAB4Ijl70f/hhiVmJftmpmXIoHZyUoyEiPSrpZAd5RfalwAAAAQAAAABAAAABgAAACVVU0QAOnN2/eiOTNYcwPspSheGs/HQYfXy8cpXRl+qkyIRuUbWAAAAAAAABAAAAAEAAAAFAAAAABHhowAAAAAAAAAAAA=="

rawXdr, err := base64.StdEncoding.DecodeString(base64xdr)
require.NoError(t, err)

event := xdr.ContractEvent{}
require.NoError(t, event.UnmarshalBinary(rawXdr))

parsed, err := NewStellarAssetContractEvent(&event, "Standalone Network ; February 2017")
assert.NoError(t, err)
assert.Equal(t, EventTypeTransfer, parsed.GetType())
}

//
// Test suite helpers below
//
Expand All @@ -204,93 +283,42 @@ func makeEvent() xdr.ContractEvent {
}

func makeTransferTopic(asset xdr.Asset) xdr.ScVec {
accountId := xdr.MustAddress(randomAccount)

fnName := xdr.ScSymbol("transfer")
account := &xdr.ScObject{
Type: xdr.ScObjectTypeScoAddress,
Address: &xdr.ScAddress{
Type: xdr.ScAddressTypeScAddressTypeAccount,
AccountId: &accountId,
},
}
contract := &xdr.ScObject{
Type: xdr.ScObjectTypeScoAddress,
Address: &xdr.ScAddress{
Type: xdr.ScAddressTypeScAddressTypeContract,
ContractId: &zeroContractHash,
},
}

slice := []byte("native")
if asset.Type != xdr.AssetTypeAssetTypeNative {
slice = []byte(asset.StringCanonical())
}
assetDetails := &xdr.ScObject{
Type: xdr.ScObjectTypeScoBytes,
Bin: &slice,
}
contractStr := strkey.MustEncode(strkey.VersionByteContract, zeroContractHash[:])

return xdr.ScVec([]xdr.ScVal{
// event name
{
Type: xdr.ScValTypeScvSymbol,
Sym: &fnName,
},
makeSymbol("transfer"),
// from
{
Type: xdr.ScValTypeScvObject,
Obj: &account,
},
makeAddress(randomAccount),
// to
{
Type: xdr.ScValTypeScvObject,
Obj: &contract,
},
makeAddress(contractStr),
// asset details
{
Type: xdr.ScValTypeScvObject,
Obj: &assetDetails,
},
makeAsset(asset),
})
}

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
topics[0] = makeSymbol("mint")
return topics
}

func makeClawbackTopic(asset xdr.Asset) xdr.ScVec {
// clawback is just mint but with a from instead of a to
fnName := xdr.ScSymbol("clawback")
topics := makeTransferTopic(asset)
topics[0].Sym = &fnName
topics[0] = makeSymbol("clawback")
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[0] = makeSymbol("burn")
topics = append(topics[:2], topics[3:]...)
return topics
}

func makeAmount(amount int) xdr.ScVal {
amountObj := &xdr.ScObject{
Type: xdr.ScObjectTypeScoI128,
I128: &xdr.Int128Parts{
Lo: xdr.Uint64(amount),
Hi: 0,
},
}

return xdr.ScVal{
Type: xdr.ScValTypeScvObject,
Obj: &amountObj,
}
func makeAmount(amount int64) xdr.ScVal {
return makeBigAmount(big.NewInt(amount))
}
Loading

0 comments on commit f8ba8f1

Please sign in to comment.