Skip to content

Commit

Permalink
Update tx indexer to include tx action outputs (#1597)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronbuchwald authored Sep 27, 2024
1 parent 6a3fd63 commit f1150a1
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 48 deletions.
9 changes: 8 additions & 1 deletion api/indexer/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ava-labs/avalanchego/trace"

"github.com/ava-labs/hypersdk/api"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/fees"
)

Expand Down Expand Up @@ -52,6 +53,7 @@ type GetTxResponse struct {
Success bool `json:"success"`
Units fees.Dimensions `json:"units"`
Fee uint64 `json:"fee"`
Outputs []codec.Bytes `json:"result"`
}

type Server struct {
Expand All @@ -63,7 +65,7 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon
_, span := s.tracer.Start(req.Context(), "Indexer.GetTx")
defer span.End()

found, t, success, units, fee, err := s.indexer.GetTransaction(args.TxID)
found, t, success, units, fee, outputs, err := s.indexer.GetTransaction(args.TxID)
if err != nil {
return err
}
Expand All @@ -75,5 +77,10 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon
reply.Success = success
reply.Units = units
reply.Fee = fee
wrappedOutputs := make([]codec.Bytes, len(outputs))
for i, output := range outputs {
wrappedOutputs[i] = codec.Bytes(output)
}
reply.Outputs = wrappedOutputs
return nil
}
64 changes: 40 additions & 24 deletions api/indexer/tx_indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
package indexer

import (
"encoding/binary"
"errors"
"path/filepath"

"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"

"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/consts"
"github.com/ava-labs/hypersdk/event"
"github.com/ava-labs/hypersdk/fees"
Expand All @@ -25,9 +25,6 @@ const (
)

var (
failureByte = byte(0x0)
successByte = byte(0x1)

_ event.SubscriptionFactory[*chain.StatefulBlock] = (*subscriptionFactory)(nil)
_ event.Subscription[*chain.StatefulBlock] = (*txDBIndexer)(nil)
)
Expand Down Expand Up @@ -103,6 +100,7 @@ func (t *txDBIndexer) Accept(blk *chain.StatefulBlock) error {
result.Success,
result.Units,
result.Fee,
result.Outputs,
); err != nil {
return err
}
Expand All @@ -122,36 +120,54 @@ func (*txDBIndexer) storeTransaction(
success bool,
units fees.Dimensions,
fee uint64,
outputs [][]byte,
) error {
v := make([]byte, consts.Uint64Len+1+fees.DimensionsLen+consts.Uint64Len)
binary.BigEndian.PutUint64(v, uint64(timestamp))
if success {
v[consts.Uint64Len] = successByte
} else {
v[consts.Uint64Len] = failureByte
outputLength := consts.ByteLen // Single byte containing number of outputs
for _, output := range outputs {
outputLength += consts.Uint32Len + len(output)
}
txResultLength := consts.Uint64Len + consts.BoolLen + fees.DimensionsLen + consts.Uint64Len + outputLength

writer := codec.NewWriter(txResultLength, txResultLength)
writer.PackUint64(uint64(timestamp))
writer.PackBool(success)
writer.PackFixedBytes(units.Bytes())
writer.PackUint64(fee)
writer.PackByte(byte(len(outputs)))
for _, output := range outputs {
writer.PackBytes(output)
}
copy(v[consts.Uint64Len+1:], units.Bytes())
binary.BigEndian.PutUint64(v[consts.Uint64Len+1+fees.DimensionsLen:], fee)
return batch.Put(txID[:], v)
if err := writer.Err(); err != nil {
return err
}
return batch.Put(txID[:], writer.Bytes())
}

func (t *txDBIndexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, error) {
func (t *txDBIndexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, [][]byte, error) {
v, err := t.db.Get(txID[:])
if errors.Is(err, database.ErrNotFound) {
return false, 0, false, fees.Dimensions{}, 0, nil
return false, 0, false, fees.Dimensions{}, 0, nil, nil
}
if err != nil {
return false, 0, false, fees.Dimensions{}, 0, err
return false, 0, false, fees.Dimensions{}, 0, nil, err
}
reader := codec.NewReader(v, consts.NetworkSizeLimit)
timestamp := reader.UnpackUint64(true)
success := reader.UnpackBool()
dimensionsBytes := make([]byte, fees.DimensionsLen)
reader.UnpackFixedBytes(fees.DimensionsLen, &dimensionsBytes)
fee := reader.UnpackUint64(true)
numOutputs := int(reader.UnpackByte())
outputs := make([][]byte, numOutputs)
for i := range outputs {
outputs[i] = reader.UnpackLimitedBytes(consts.NetworkSizeLimit)
}
timestamp := int64(binary.BigEndian.Uint64(v))
success := true
if v[consts.Uint64Len] == failureByte {
success = false
if err := reader.Err(); err != nil {
return false, 0, false, fees.Dimensions{}, 0, nil, err
}
d, err := fees.UnpackDimensions(v[consts.Uint64Len+1 : consts.Uint64Len+1+fees.DimensionsLen])
dimensions, err := fees.UnpackDimensions(dimensionsBytes)
if err != nil {
return false, 0, false, fees.Dimensions{}, 0, err
return false, 0, false, fees.Dimensions{}, 0, nil, err
}
fee := binary.BigEndian.Uint64(v[consts.Uint64Len+1+fees.DimensionsLen:])
return true, timestamp, success, d, fee, nil
return true, int64(timestamp), success, dimensions, fee, outputs, nil
}
8 changes: 4 additions & 4 deletions codec/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ func ToAddress(b []byte) (Address, error) {
// StringToAddress uses copy, which copies the minimum of
// either AddressLen or the length of the hex decoded string.
func StringToAddress(s string) (Address, error) {
b, err := hex.DecodeString(s)
var a Address
b, err := LoadHex(s, AddressLen)
if err != nil {
return Address{}, fmt.Errorf("failed to convert hex string to address: %w", err)
return a, err
}
var a Address
copy(a[:], b)
return a, nil
}

// String implements fmt.Stringer.
func (a Address) String() string {
return hex.EncodeToString(a[:])
return ToHex(a[:])
}

// MarshalText returns the hex representation of a.
Expand Down
17 changes: 17 additions & 0 deletions codec/hex.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,20 @@ func LoadHex(s string, expectedSize int) ([]byte, error) {
}
return bytes, nil
}

type Bytes []byte

// MarshalText returns the hex representation of b.
func (b Bytes) MarshalText() ([]byte, error) {
return []byte(ToHex(b)), nil
}

// UnmarshalText sets b to the bytes represented by text.
func (b *Bytes) UnmarshalText(text []byte) error {
bytes, err := LoadHex(string(text), -1)
if err != nil {
return err
}
*b = bytes
return nil
}
31 changes: 31 additions & 0 deletions codec/hex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package codec

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestBytesHex(t *testing.T) {
require := require.New(t)
b := []byte{1, 2, 3, 4, 5}
wrappedBytes := Bytes(b)

marshalledBytes, err := wrappedBytes.MarshalText()
require.NoError(err)

var unmarshalledBytes Bytes
require.NoError(unmarshalledBytes.UnmarshalText(marshalledBytes))
require.Equal(b, []byte(unmarshalledBytes))

jsonMarshalledBytes, err := json.Marshal(wrappedBytes)
require.NoError(err)

var jsonUnmarshalledBytes Bytes
require.NoError(json.Unmarshal(jsonMarshalledBytes, &jsonUnmarshalledBytes))
require.Equal(b, []byte(jsonUnmarshalledBytes))
}
46 changes: 27 additions & 19 deletions examples/morpheusvm/tests/workload/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/crypto/secp256r1"
"github.com/ava-labs/hypersdk/examples/morpheusvm/actions"
"github.com/ava-labs/hypersdk/examples/morpheusvm/consts"
"github.com/ava-labs/hypersdk/examples/morpheusvm/vm"
"github.com/ava-labs/hypersdk/fees"
"github.com/ava-labs/hypersdk/genesis"
Expand Down Expand Up @@ -134,15 +135,7 @@ func (g *simpleTxWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain.
}

return tx, func(ctx context.Context, require *require.Assertions, uri string) {
indexerCli := indexer.NewClient(uri)
success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, tx.ID())
require.NoError(err)
require.True(success)
lcli := vm.NewJSONRPCClient(uri)
balance, err := lcli.Balance(ctx, aother)
require.NoError(err)
require.Equal(uint64(1), balance)
// TODO: check transaction output (not currently available via API)
confirmTx(ctx, require, uri, tx.ID(), aother, 1)
}, nil
}

Expand Down Expand Up @@ -231,15 +224,30 @@ func (g *mixedAuthWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain
g.balance = expectedBalance

return tx, func(ctx context.Context, require *require.Assertions, uri string) {
indexerCli := indexer.NewClient(uri)
success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, tx.ID())
require.NoError(err)
require.True(success)
lcli := vm.NewJSONRPCClient(uri)
balance, err := lcli.Balance(ctx, receiver.address)
require.NoError(err)
require.Equal(expectedBalance, balance)
// TODO: check tx fee + units (not currently available via API)
// TODO: check transaction output (not currently available via API)
confirmTx(ctx, require, uri, tx.ID(), receiver.address, expectedBalance)
}, nil
}

func confirmTx(ctx context.Context, require *require.Assertions, uri string, txID ids.ID, receiverAddr codec.Address, receiverExpectedBalance uint64) {
indexerCli := indexer.NewClient(uri)
success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, txID)
require.NoError(err)
require.True(success)
lcli := vm.NewJSONRPCClient(uri)
balance, err := lcli.Balance(ctx, receiverAddr)
require.NoError(err)
require.Equal(receiverExpectedBalance, balance)
txRes, _, err := indexerCli.GetTx(ctx, txID)
require.NoError(err)
// TODO: perform exact expected fee, units check, and output check
require.NotZero(txRes.Fee)
require.Len(txRes.Outputs, 1)
transferOutputBytes := []byte(txRes.Outputs[0])
require.Equal(consts.TransferID, transferOutputBytes[0])
reader := codec.NewReader(transferOutputBytes, len(transferOutputBytes))
transferOutputTyped, err := vm.OutputParser.Unmarshal(reader)
require.NoError(err)
transferOutput, ok := transferOutputTyped.(*actions.TransferResult)
require.True(ok)
require.Equal(receiverExpectedBalance, transferOutput.ReceiverBalance)
}

0 comments on commit f1150a1

Please sign in to comment.