add Client interface and MockClient for unit tests

This commit is contained in:
chanhle 2023-10-25 22:03:47 -04:00
parent 494d69c269
commit f778e8b1fc
7 changed files with 285 additions and 15 deletions

View file

@ -0,0 +1,72 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package algorand
import (
"context"
"fmt"
"github.com/algorand/go-algorand-sdk/client/v2/algod"
"github.com/algorand/go-algorand-sdk/client/v2/common/models"
)
// Client is the interface for the Algorand client.
type Client interface {
// GetAccount returns the account information.
GetAccount(address string) (*models.Account, error)
// CheckStatus checks the status of the client.
CheckStatus() error
}
// AlgorandClient implements the Client interface.
type AlgorandClient struct {
algodAddress string
algodToken string
algodClient *algod.Client
}
// NewClient creates a new Algorand client.
func NewClient(algodAddress, algodToken string) *AlgorandClient {
algorandClient := &AlgorandClient{
algodAddress: algodAddress,
algodToken: algodToken,
}
algodClient, err := algod.MakeClient(algodAddress, algodToken)
if err == nil {
algorandClient.algodClient = algodClient
}
return algorandClient
}
func (c *AlgorandClient) CheckStatus() error {
if c.algodAddress == "" {
return fmt.Errorf("algodAddress is not set")
}
if c.algodToken == "" {
return fmt.Errorf("algodToken is not set")
}
if c.algodClient == nil {
return fmt.Errorf("algodClient is not initialized")
}
return nil
}
// GetAccount returns the account information.
func (c *AlgorandClient) GetAccount(address string) (*models.Account, error) {
account, err := c.algodClient.AccountInformation(address).Do(context.Background())
return &account, err
}

View file

@ -93,6 +93,10 @@ func abiType(name string, typ reflect.Type) string {
fields = append(fields, abiType(typ.Field(i).Name, typ.Field(i).Type)) fields = append(fields, abiType(typ.Field(i).Name, typ.Field(i).Type))
} }
return strings.Join(fields, ", ") return strings.Join(fields, ", ")
} else if typ.Kind() == reflect.Int {
return fmt.Sprintf(`{"name": "%s", "type": "int256"}`, name)
} else if typ.Kind() == reflect.Uint {
return fmt.Sprintf(`{"name": "%s", "type": "uint256"}`, name)
} else { } else {
return fmt.Sprintf(`{"name": "%s", "type": "%s"}`, name, typ.String()) return fmt.Sprintf(`{"name": "%s", "type": "%s"}`, name, typ.String())
} }

View file

@ -26,12 +26,23 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// pack encodes the value into bytes.
func pack(value reflect.Value) ([]byte, error) { func pack(value reflect.Value) ([]byte, error) {
// Convert int to int64 or uint to uint64.
// This is because the ABI does not support int or uint.
// The cast is safe because int/uint is either 32-bit or 64-bit.
if value.Kind() == reflect.Int {
value = reflect.ValueOf(int64(value.Int()))
} else if value.Kind() == reflect.Uint {
value = reflect.ValueOf(uint64(value.Uint()))
}
defn := fmt.Sprintf(`[{"type": "constructor", "inputs": [%s]}]`, abiType("", value.Type())) defn := fmt.Sprintf(`[{"type": "constructor", "inputs": [%s]}]`, abiType("", value.Type()))
abi, err := abi.JSON(strings.NewReader(defn)) abi, err := abi.JSON(strings.NewReader(defn))
if err != nil { if err != nil {
return nil, err return nil, err
} }
data, err := abi.Pack("", value.Interface()) data, err := abi.Pack("", value.Interface())
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -0,0 +1,112 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package algorand
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func packValue(v any) (any, error) {
data, err := pack(reflect.ValueOf(v))
if err != nil {
return nil, err
}
var vv reflect.Value
switch v.(type) {
case int:
vv = reflect.New(reflect.TypeOf(int64(0)))
case uint:
vv = reflect.New(reflect.TypeOf(uint64(0)))
default:
vv = reflect.New(reflect.TypeOf(v))
}
err = unpack(data, vv.Interface())
if err != nil {
return nil, err
}
return vv.Elem().Interface(), nil
}
func TestPackBool(t *testing.T) {
v, err := packValue(true)
require.NoError(t, err)
require.Equal(t, true, v)
v, err = packValue(false)
require.NoError(t, err)
require.Equal(t, false, v)
}
func TestPackString(t *testing.T) {
v, err := packValue("hello")
require.NoError(t, err)
require.Equal(t, "hello", v)
}
func TestPackByte(t *testing.T) {
v, err := packValue(byte('a'))
require.NoError(t, err)
require.Equal(t, byte('a'), v)
}
func TestPackInteger(t *testing.T) {
i := 1000000
v, err := packValue(int(i))
require.NoError(t, err)
require.Equal(t, int64(i), v)
v, err = packValue(int8(i))
require.NoError(t, err)
require.Equal(t, int8(i), v)
v, err = packValue(int16(i))
require.NoError(t, err)
require.Equal(t, int16(i), v)
v, err = packValue(int32(i))
require.NoError(t, err)
require.Equal(t, int32(i), v)
v, err = packValue(int64(i))
require.NoError(t, err)
require.Equal(t, int64(i), v)
v, err = packValue(uint(i))
require.NoError(t, err)
require.Equal(t, uint64(i), v)
v, err = packValue(uint8(i))
require.NoError(t, err)
require.Equal(t, uint8(i), v)
v, err = packValue(uint16(i))
require.NoError(t, err)
require.Equal(t, uint16(i), v)
v, err = packValue(uint32(i))
require.NoError(t, err)
require.Equal(t, uint32(i), v)
v, err = packValue(uint64(i))
require.NoError(t, err)
require.Equal(t, uint64(i), v)
}

View file

@ -17,34 +17,29 @@
package algorand package algorand
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
"strings"
"github.com/algorand/go-algorand-sdk/client/v2/algod"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
// Algorand implements the precompile to access the Algorand blockchain. // Algorand implements the precompile to access the Algorand blockchain.
type Algorand struct { type Algorand struct {
algodAddress string algodClient Client
algodToken string
algodClient *algod.Client
} }
// New creates a new Algorand precompiled contract with a client to access Algorand blockchain. // New creates a new Algorand precompiled contract with a client to access Algorand blockchain.
func New() *Algorand { func New() *Algorand {
algodAddress := os.Getenv("ALGOD_ADDRESS") algodAddress := os.Getenv("ALGOD_ADDRESS")
algodToken := os.Getenv("ALGOD_TOKEN") algodToken := os.Getenv("ALGOD_TOKEN")
algorand := &Algorand{ if !strings.HasPrefix(algodAddress, "http://") {
algodAddress: algodAddress, algodAddress = "http://" + algodAddress
algodToken: algodToken,
} }
algodClient, err := algod.MakeClient(algodAddress, algodToken) algorand := &Algorand{
if err == nil { algodClient: NewClient(algodAddress, algodToken),
algorand.algodClient = algodClient
} }
return algorand return algorand
} }
@ -56,8 +51,9 @@ func (a *Algorand) RequiredGas(input []byte) uint64 {
// Run executes the Algorand precompile with the given input. // Run executes the Algorand precompile with the given input.
func (a *Algorand) Run(input []byte) ([]byte, error) { func (a *Algorand) Run(input []byte) ([]byte, error) {
if a.algodClient == nil { err := a.algodClient.CheckStatus()
return nil, fmt.Errorf("cannot connect to Algorand node") if err != nil {
return nil, err
} }
params, err := UnpackInput(input) params, err := UnpackInput(input)
@ -67,13 +63,13 @@ func (a *Algorand) Run(input []byte) ([]byte, error) {
var info interface{} var info interface{}
switch params.GetCmdType() { switch params.GetCmdType() {
case AccountCmd: case AccountCmd:
info, err = a.algodClient.AccountInformation(params.(*AccountInput).Address).Do(context.Background()) info, err = a.algodClient.GetAccount(params.(*AccountInput).Address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
log.Info("Algorand.Run", "info", info) log.Info("Algorand.Run", "info", info)
value := reflect.ValueOf(info).FieldByName(params.GetFieldName()) value := reflect.ValueOf(info).Elem().FieldByName(params.GetFieldName())
if !value.IsValid() { if !value.IsValid() {
return nil, fmt.Errorf("field %s does not exist", params.GetFieldName()) return nil, fmt.Errorf("field %s does not exist", params.GetFieldName())
} }

View file

@ -0,0 +1,73 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package algorand
import (
"fmt"
"testing"
"github.com/algorand/go-algorand-sdk/client/v2/common/models"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
// MockClient is a mock client for testing.
type MockClient struct {
// store is mapping from Algorand addresses to Algorand accounts.
store map[string]*models.Account
}
// NewMockClient creates a new mock client.
func NewMockClient() *MockClient {
store := make(map[string]*models.Account)
store["737777777777777777777777777777777777777777777777777UFEJ2CI"] = &models.Account{
Address: "737777777777777777777777777777777777777777777777777UFEJ2CI",
Amount: 10000000,
}
return &MockClient{
store: store,
}
}
// GetAccount returns the account information.
func (c *MockClient) GetAccount(address string) (*models.Account, error) {
if _, ok := c.store[address]; !ok {
return nil, fmt.Errorf("account not found")
}
return c.store[address], nil
}
// CheckStatus of MockClient is always successful.
func (c *MockClient) CheckStatus() error {
return nil
}
// TestRun tests the Run function on an Algorand object with a MockClient.
func TestRun(t *testing.T) {
algorand := &Algorand{
algodClient: NewMockClient(),
}
rawInput, err := common.ParseHexOrString("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000006416d6f756e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a3733373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737375546454a324349000000000000")
require.NoError(t, err)
rawOutput, err := algorand.Run(rawInput)
require.NoError(t, err)
v := new(uint64)
err = unpack(rawOutput, v)
require.NoError(t, err)
require.Equal(t, uint64(10000000), *v)
}

View file

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/crypto/bls12381" "github.com/ethereum/go-ethereum/crypto/bls12381"
"github.com/ethereum/go-ethereum/crypto/bn256" "github.com/ethereum/go-ethereum/crypto/bn256"
"github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"golang.org/x/crypto/ripemd160" "golang.org/x/crypto/ripemd160"
) )
@ -177,6 +178,7 @@ func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uin
} }
suppliedGas -= gasCost suppliedGas -= gasCost
output, err := p.Run(input) output, err := p.Run(input)
log.Info("RunPrecompiledContract", "err", err)
return output, suppliedGas, err return output, suppliedGas, err
} }