Skip to content

Commit

Permalink
feat(builtins): add typed contracts; refactor(thorgen): separate txs …
Browse files Browse the repository at this point in the history
…and read-only
  • Loading branch information
darrenvechain committed Nov 2, 2024
1 parent 87c9d05 commit 3e634b4
Show file tree
Hide file tree
Showing 31 changed files with 4,010 additions and 357 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
vendor
.idea
coverage.out
cmd/test
157 changes: 79 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,71 +101,73 @@ type Signer interface {
package main

import (
"fmt"

"github.com/darrenvechain/thorgo"
"github.com/darrenvechain/thorgo/solo"
"github.com/ethereum/go-ethereum/common"
"fmt"
"github.com/darrenvechain/thorgo"
"github.com/darrenvechain/thorgo/solo"
"github.com/ethereum/go-ethereum/common"
)

func main() {
// Create a new client
thor, err := thorgo.NewFromURL(solo.URL)

// Get an accounts balance
acc, err := thor.Account(common.HexToAddress("0x0000000000000000000000000000456e6570")).Get()
fmt.Println(acc.Balance)
// Create a new client
thor := thorgo.New(solo.URL)
// Get an accounts balance
acc, err := thor.Account(common.HexToAddress("0x0000000000000000000000000000456e6570")).Get()
fmt.Println(acc.Balance)
}
```

### 2: Interacting with a contract + Delegated Transaction

- It is recommended to create your smart contract wrapper using the `thorgen` CLI. This provides a more idiomatic way to
interact with the contract.

<details>
<summary>Expand</summary>

```golang
package main

import (
"log/slog"
"math/big"

"github.com/darrenvechain/thorgo"
"github.com/darrenvechain/thorgo/builtins"
"github.com/darrenvechain/thorgo/solo"
"github.com/darrenvechain/thorgo/txmanager"
"github.com/ethereum/go-ethereum/common"
"log/slog"
"math/big"

"github.com/darrenvechain/thorgo"
"github.com/darrenvechain/thorgo/builtins"
"github.com/darrenvechain/thorgo/solo"
"github.com/darrenvechain/thorgo/txmanager"
)

func main() {
thor, _ := thorgo.NewFromURL("http://localhost:8669")

// Load a contract
vtho := thor.Account(common.HexToAddress("0x0000000000000000000000000000456e65726779")).Contract(builtins.VTHO.ABI)

// Create a delegated transaction manager
origin := txmanager.FromPK(solo.Keys()[0], thor)
gasPayer := txmanager.NewDelegator(solo.Keys()[1])
txSender := txmanager.NewDelegatedManager(thor, origin, gasPayer)

// Create a new account to receive the tokens
recipient, _ := txmanager.GeneratePK(thor)
recipientBalance := new(big.Int)

// Call the balanceOf function
err := vtho.Call("balanceOf", &recipientBalance, recipient.Address())
slog.Info("recipient balance before", "balance", recipientBalance, "error", err)

// Send 1000 tokens to the recipient
tx, _ := vtho.Send(txSender, "transfer", recipient.Address(), big.NewInt(1000))
receipt, _ := tx.Wait()
slog.Info("receipt", "txID", receipt.Meta.TxID, "reverted", receipt.Reverted)

// Call the balanceOf function again
err = vtho.Call("balanceOf", &recipientBalance, recipient.Address())
slog.Info("recipient balance after", "balance", recipientBalance, "error", err)
thor := thorgo.New("http://localhost:8669")

// Create a delegated transaction manager
origin := txmanager.FromPK(solo.Keys()[0], thor)
gasPayer := txmanager.NewDelegator(solo.Keys()[1])
txSender := txmanager.NewDelegatedManager(thor, origin, gasPayer)

// Use the `thorgen` CLI to build your own smart contract wrapper
vtho, _ := builtins.NewEnergyTransactor(thor, txSender)

// Create a new account to receive the tokens
recipient, _ := txmanager.GeneratePK(thor)

// Call the balanceOf function
balance, err := vtho.BalanceOf(recipient.Address())
slog.Info("recipient balance before", "balance", balance, "error", err)

tx, err := vtho.Transfer(recipient.Address(), big.NewInt(1000000000000000000))
if err != nil {
slog.Error("transfer error", "error", err)
return
}
receipt, err := tx.Wait()
slog.Info("transfer receipt", "error", err != nil || receipt.Reverted)

balance, err = vtho.BalanceOf(recipient.Address())
slog.Info("recipient balance after", "balance", balance, "error", err)
}

```

</details>
Expand All @@ -179,41 +181,40 @@ func main() {
package main

import (
"log/slog"
"math/big"

"github.com/darrenvechain/thorgo"
"github.com/darrenvechain/thorgo/builtins"
"github.com/darrenvechain/thorgo/crypto/tx"
"github.com/darrenvechain/thorgo/solo"
"github.com/darrenvechain/thorgo/txmanager"
"github.com/ethereum/go-ethereum/common"
"log/slog"
"math/big"

"github.com/darrenvechain/thorgo"
"github.com/darrenvechain/thorgo/builtins"
"github.com/darrenvechain/thorgo/crypto/tx"
"github.com/darrenvechain/thorgo/solo"
"github.com/darrenvechain/thorgo/txmanager"
)

func main() {
thor, _ := thorgo.NewFromURL("http://localhost:8669")

// Load a contract
vtho := thor.Account(common.HexToAddress("0x0000000000000000000000000000456e65726779")).Contract(builtins.VTHO.ABI)

origin := txmanager.FromPK(solo.Keys()[0], thor)

// clause1
clause1, _ := vtho.AsClause("transfer", common.HexToAddress("0x87AA2B76f29583E4A9095DBb6029A9C41994E25B"), big.NewInt(1000000))
clause2, _ := vtho.AsClause("transfer", common.HexToAddress("0xdf1b32ec78c1f338F584a2a459f01fD70529dDBF"), big.NewInt(1000000))

// Option 1 - Directly using the txmanager.Manager
trx, _ := origin.SendClauses([]*tx.Clause{clause1, clause2})
receipt, _ := thor.Transaction(trx).Wait()
slog.Info("transaction receipt 1", "id", receipt.Meta.TxID, "reverted", receipt.Reverted)

// Option 2 - Using the transaction builder with txmanager.Signer
tx2, _ := thor.Transactor([]*tx.Clause{clause1, clause2}).
GasPriceCoef(255).
Send(origin)
receipt2, _ := tx2.Wait()
slog.Info("transaction receipt 2", "id", receipt2.Meta.TxID, "reverted", receipt2.Reverted)
thor := thorgo.New("http://localhost:8669")
// Create a delegated transaction manager
origin := txmanager.FromPK(solo.Keys()[0], thor)
recipient1, _ := txmanager.GeneratePK(thor)
recipient2, _ := txmanager.GeneratePK(thor)
vtho, _ := builtins.NewEnergyTransactor(thor, origin)
clause1, _ := vtho.TransferAsClause(recipient1.Address(), big.NewInt(1000))
clause2, _ := vtho.TransferAsClause(recipient2.Address(), big.NewInt(9999))
txID, _ := origin.SendClauses([]*tx.Clause{clause1, clause2})
slog.Info("transaction sent", "id", txID)
trx, _ := thor.Transaction(txID).Wait()
slog.Info("transaction mined", "reverted", trx.Reverted)
balance1, _ := vtho.BalanceOf(recipient1.Address())
balance2, _ := vtho.BalanceOf(recipient2.Address())
slog.Info("recipient1", "balance", balance1)
slog.Info("recipient2", "balance", balance2)
}
```

```
</details>
6 changes: 3 additions & 3 deletions accounts/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import (
type Visitor struct {
client *client.Client
account common.Address
revision *common.Hash
revision *client.Revision
}

func New(c *client.Client, account common.Address) *Visitor {
return &Visitor{client: c, account: account}
}

// Revision sets the optional revision for the API calls.
func (a *Visitor) Revision(revision common.Hash) *Visitor {
func (a *Visitor) Revision(revision client.Revision) *Visitor {
a.revision = &revision
return a
}
Expand Down Expand Up @@ -78,5 +78,5 @@ func (a *Visitor) Call(calldata []byte) (*client.InspectResponse, error) {

// Contract returns a new Contract instance.
func (a *Visitor) Contract(abi *abi.ABI) *Contract {
return NewContractAt(a.client, a.account, abi, a.revision)
return NewContract(a.client, a.account, abi)
}
22 changes: 14 additions & 8 deletions accounts/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import (
var (
thorClient *client.Client
thor *thorgo.Thor
vthoContract *accounts.Contract
vtho = builtins.VTHO
vthoContract *builtins.Energy
vthoRaw *accounts.Contract
account1 *txmanager.PKManager
)

Expand All @@ -27,7 +27,9 @@ func TestMain(m *testing.M) {
thorClient, cancel = testcontainer.NewSolo()
defer cancel()
thor = thorgo.NewFromClient(thorClient)
vthoContract = vtho.Load(thor)
vthoContract, _ = builtins.NewEnergy(thor)
abi, _ := builtins.EnergyMetaData.GetAbi()
vthoRaw = accounts.NewContract(thorClient, vthoContract.Address(), abi)
account1 = txmanager.FromPK(solo.Keys()[0], thor)
m.Run()
}
Expand All @@ -47,7 +49,7 @@ func TestGetAccount(t *testing.T) {
// TestGetAccountForRevision fetches a thor solo account for the genesis block
// and checks if the balance and energy are greater than 0
func TestGetAccountForRevision(t *testing.T) {
acc, err := accounts.New(thorClient, account1.Address()).Revision(solo.GenesisID()).Get()
acc, err := accounts.New(thorClient, account1.Address()).Revision(client.RevisionID(solo.GenesisID())).Get()

assert.NoError(t, err, "Account.httpGet should not return an error")
assert.NotNil(t, acc, "Account.httpGet should return an account")
Expand All @@ -59,7 +61,7 @@ func TestGetAccountForRevision(t *testing.T) {

// TestGetCode fetches the code of the VTHO contract and checks if the code length is greater than 2 (0x)
func TestGetCode(t *testing.T) {
vtho, err := accounts.New(thorClient, vtho.Address).Code()
vtho, err := accounts.New(thorClient, vthoContract.Address()).Code()

assert.NoError(t, err, "Account.Code should not return an error")
assert.NotNil(t, vtho, "Account.Code should return a code")
Expand All @@ -68,7 +70,9 @@ func TestGetCode(t *testing.T) {

// TestGetCodeForRevision fetches the code of the VTHO contract for the genesis block
func TestGetCodeForRevision(t *testing.T) {
vtho, err := accounts.New(thorClient, vtho.Address).Revision(solo.GenesisID()).Code()
vtho, err := accounts.New(thorClient, vthoContract.Address()).
Revision(client.RevisionID(solo.GenesisID())).
Code()

assert.NoError(t, err, "Account.Code should not return an error")
assert.NotNil(t, vtho, "Account.Code should return a code")
Expand All @@ -77,7 +81,7 @@ func TestGetCodeForRevision(t *testing.T) {

// TestGetStorage fetches a storage position of the VTHO contract and checks if the value is empty
func TestGetStorage(t *testing.T) {
storage, err := accounts.New(thorClient, vtho.Address).Storage(common.Hash{})
storage, err := accounts.New(thorClient, vthoContract.Address()).Storage(common.Hash{})

assert.NoError(t, err, "Account.Storage should not return an error")
assert.NotNil(t, storage, "Account.Storage should return a storage")
Expand All @@ -86,7 +90,9 @@ func TestGetStorage(t *testing.T) {

// TestGetStorageForRevision fetches a storage position of the VTHO contract for the genesis block
func TestGetStorageForRevision(t *testing.T) {
storage, err := accounts.New(thorClient, vtho.Address).Revision(solo.GenesisID()).Storage(common.Hash{})
storage, err := accounts.New(thorClient, vthoContract.Address()).
Revision(client.RevisionID(solo.GenesisID())).
Storage(common.Hash{})

assert.NoError(t, err, "Account.Storage should not return an error")
assert.NotNil(t, storage, "Account.Storage should return a storage")
Expand Down
60 changes: 34 additions & 26 deletions accounts/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ import (

// Contract is a generic representation of a smart contract.
type Contract struct {
client *client.Client
revision *common.Hash
ABI *abi.ABI
Address common.Address
client *client.Client
ABI *abi.ABI
Address common.Address
}

// NewContract creates a new contract instance.
Expand All @@ -28,21 +27,19 @@ func NewContract(
address common.Address,
abi *abi.ABI,
) *Contract {
return &Contract{client: client, Address: address, ABI: abi, revision: nil}
return &Contract{client: client, Address: address, ABI: abi}
}

// NewContractAt creates a new contract instance at a specific revision. It should be used to query historical contract states.
func NewContractAt(
client *client.Client,
address common.Address,
abi *abi.ABI,
revision *common.Hash,
) *Contract {
return &Contract{client: client, Address: address, ABI: abi, revision: revision}
// Call executes a read-only contract call.
func (c *Contract) Call(method string, results *[]interface{}, args ...interface{}) error {
return c.CallAt(client.RevisionBest(), method, results, args...)
}

// Call executes a read-only contract call.
func (c *Contract) Call(method string, value interface{}, args ...interface{}) error {
// CallAt executes a read-only contract call at a specific revision.
func (c *Contract) CallAt(revision client.Revision, method string, results *[]interface{}, args ...interface{}) error {
if results == nil {
results = new([]interface{})
}
packed, err := c.ABI.Pack(method, args...)
if err != nil {
return fmt.Errorf("failed to pack method %s: %w", method, err)
Expand All @@ -51,12 +48,7 @@ func (c *Contract) Call(method string, value interface{}, args ...interface{}) e
request := client.InspectRequest{
Clauses: []*tx.Clause{clause},
}
var response []client.InspectResponse
if c.revision == nil {
response, err = c.client.Inspect(request)
} else {
response, err = c.client.InspectAt(request, *c.revision)
}
response, err := c.client.InspectAt(request, revision)
if err != nil {
return fmt.Errorf("failed to inspect contract: %w", err)
}
Expand All @@ -71,11 +63,13 @@ func (c *Contract) Call(method string, value interface{}, args ...interface{}) e
if err != nil {
return fmt.Errorf("failed to decode data: %w", err)
}
err = c.ABI.UnpackIntoInterface(value, method, decoded)
if err != nil {
return fmt.Errorf("failed to unpack method %s: %w", method, err)
if len(*results) == 0 {
res, err := c.ABI.Unpack(method, decoded)
*results = res
return err
}
return nil
res := *results
return c.ABI.UnpackIntoInterface(res[0], method, decoded)
}

// DecodeCall decodes the result of a contract call, for example, decoding a clause's 'data'.
Expand Down Expand Up @@ -111,13 +105,27 @@ func (c *Contract) AsClause(method string, args ...interface{}) (*tx.Clause, err
return tx.NewClause(&c.Address).WithData(packed).WithValue(big.NewInt(0)), nil
}

// AsClauseWithVET returns a transaction clause for the given method, value, and arguments.
func (c *Contract) AsClauseWithVET(vet *big.Int, method string, args ...interface{}) (*tx.Clause, error) {
packed, err := c.ABI.Pack(method, args...)
if err != nil {
return nil, fmt.Errorf("failed to pack method %s: %w", method, err)
}
return tx.NewClause(&c.Address).WithData(packed).WithValue(vet), nil
}

type TxManager interface {
SendClauses(clauses []*tx.Clause) (common.Hash, error)
}

// Send executes a transaction with a single clause.
func (c *Contract) Send(manager TxManager, method string, args ...interface{}) (*transactions.Visitor, error) {
clause, err := c.AsClause(method, args...)
return c.SendWithVET(manager, big.NewInt(0), method, args...)
}

// SendWithVET executes a transaction with a single clause and a value.
func (c *Contract) SendWithVET(manager TxManager, vet *big.Int, method string, args ...interface{}) (*transactions.Visitor, error) {
clause, err := c.AsClauseWithVET(vet, method, args...)
if err != nil {
return &transactions.Visitor{}, fmt.Errorf("failed to pack method %s: %w", method, err)
}
Expand Down
Loading

0 comments on commit 3e634b4

Please sign in to comment.