Skip to content

Commit

Permalink
feat: transaction builder
Browse files Browse the repository at this point in the history
  • Loading branch information
hanchon committed Jul 28, 2024
1 parent ba79ad4 commit 807031f
Show file tree
Hide file tree
Showing 46 changed files with 1,335 additions and 7 deletions.
44 changes: 38 additions & 6 deletions docs/pages/lib/requester/web3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

List of supported rest calls to the Web3 endpoint

## ERC20 Integration

The web3 requester integrates with the [`ERC20` module](/lib/smartcontract/erc20)

The `GetTotalSupply` and `GetContractBalance` can be used directly from the `Client`

```go
supply, err := c.GetTotalSupply(contractAddress, height)
balance, err := c.GetContractBalance(contractAddress, wallet, height)
```

## GetBlockByNumber

Sending the height and whether you need the transactions or not will return the block data.
Expand All @@ -20,19 +31,40 @@ c.GetTransactionTrace(hash)

## GetTransactionReceipt

Get the receipt for a given transaction hash
Gets the receipt for a given transaction hash

```go
c.GetTransactionReceipt(hash)
```

## ERC20 Integration
## Get Nonce

The web3 requester integrates with the [`ERC20` module](/lib/smartcontract/erc20)
Gets the current nonce for an account

The `GetTotalSupply` and `GetContractBalance` can be used directly from the `Client`
```go
nonce, err := c.GetNonce(address)
```

## GasPrice

Gets current network's gas price

```go
supply, err := c.GetTotalSupply(contractAddress, height)
balance, err := c.GetContractBalance(contractAddress, wallet, height)
gasPrice, err := c.GasPrice()
```

## NetworkID

Gets network's chain ID

```go
chainID, err := c.ChainID()
```

## Send Transaction

Broadcast the transaction object

```go
txHash, err := c.BroadcastTX(tx)
```
11 changes: 11 additions & 0 deletions docs/pages/lib/txbuilder/contract.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Contract

The contract struct is used internally to associate an address and an ABI, so we can easily interact with the deployed contract.

## New Contract

This function is used mostly to instantiate the TxBuilder with the list of contracts that can be interacted with.

```go
contract := NewContract(address, abi)
```
5 changes: 4 additions & 1 deletion docs/pages/lib/txbuilder/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

A web3 transaction builder

- [Wallet](/lib/txbuilder/wallet.go)
- [Wallet](/lib/txbuilder/wallet)
- [Contract](/lib/txbuilder/contract)
- [Transaction Builder](/lib/txbuilder/txbuilder)
- [Transaction](/lib/txbuilder/transaction)
29 changes: 29 additions & 0 deletions docs/pages/lib/txbuilder/transaction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Transaction

To create and send transactions, we need some on-chain info, encode the data correctly and broadcast the transaction to the network.

## Send Transaction

The `SendTransaction` function takes care of building the correct transaction and broadcasting it.

```go
txHash, err := t.SendTransaction(contractName, address, privateKey, value, message, args)
```

:::info
This function is meant to be used just internally by the `InteractWithContract` function.
:::

## Send Transaction to Smart Contracts

This helper function makes it easy to send a transaction to a smart contract, it automatically instantie your wallet based on the `TxBuilder` mnemonic and the account ID.

```go
txHash, err := t.InteractWithContract(contractName, accountID, value, message, args)
```

:::info
The `contractName` variable is the string set in the TxBuilder constructor for the contract.

The `accountID` is the id related to the path of the `mnemonic` usually you just need to use the id `0`
:::
23 changes: 23 additions & 0 deletions docs/pages/lib/txbuilder/txbuilder.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Transaction Builder

To create a transaction first you need to create a TxBuilder object.

## Constructor

Using the `NewTxBuilder` you can instantiate the TxBuilder.

```go
builder := NexTxBuilder(
contracts: map[string]Contract{"erc20": NewContract{address, abi}},
mnemonic: mnemonic,
customGasLimit: map[string]uint64{},
defaultGasLimit: uint64(200_000),
requester: requester.NewClient().WithUnsecureWeb3Endpoint("https://proxy.evmos.org/web3"),
)
```

:::info
`customGasLimit` allows you to set a custom gas limit for different `methods`, i.e. `map[string]uint64{"transfer":uint64(20_000)}`
:::

After having the builder instantiated, you can send [transactions](/lib/txbuilder/transaction)
79 changes: 79 additions & 0 deletions lib/requester/web3.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package requester

import (
"fmt"
"math/big"
"strconv"

"github.com/ethereum/go-ethereum/common/hexutil"
coretypes "github.com/ethereum/go-ethereum/core/types"
web3types "github.com/hanchon/hanchond/lib/types/web3"
)

Expand Down Expand Up @@ -35,3 +39,78 @@ func (c *Client) GetTransactionReceipt(hash string) (*web3types.TxReceipt, error
c.Web3Auth,
)
}

func (c *Client) GetNonce(address string) (uint64, error) {
var resp web3types.NonceResponse
if err := c.SendPostRequestEasyJSON(
c.Web3Endpoint,
[]byte(`{"method":"eth_getTransactionCount","params":["`+address+`", "latest"],"id":1,"jsonrpc":"2.0"}`),
&resp,
c.Web3Auth,
); err != nil {
return 0, err
}
return strconv.ParseUint(resp.Result, 0, 64)
}

func (c *Client) GasPrice() (*big.Int, error) {
var resp web3types.GasPriceResponse
if err := c.SendPostRequestEasyJSON(
c.Web3Endpoint,
[]byte(`{"method":"eth_gasPrice","params":[],"id":1,"jsonrpc":"2.0"}`),
&resp,
c.Web3Auth,
); err != nil {
return nil, err
}

supply := new(big.Int)
supply.SetString(resp.Result[2:], 16)
return supply, nil
}

func (c *Client) ChanID() (*big.Int, error) {
var resp web3types.NetVersionResponse
if err := c.SendPostRequestEasyJSON(
c.Web3Endpoint,
[]byte(`{"method":"net_version","params":[],"id":1,"jsonrpc":"2.0"}`),
&resp,
c.Web3Auth,
); err != nil {
return nil, err
}

version := new(big.Int)
if _, ok := version.SetString(resp.Result, 10); !ok {
return nil, fmt.Errorf("invalid chainID %q", resp.Result)
}

return version, nil
}

// BroadcastTx returns txhash and error
func (c *Client) BroadcastTx(tx *coretypes.Transaction) (string, error) {
var resp web3types.SendRawTransactionResponse
data, err := tx.MarshalBinary()
if err != nil {
return "", err
}

if err := c.SendPostRequestEasyJSON(
c.Web3Endpoint,
[]byte(`{"method":"eth_sendRawTransaction","params":["`+hexutil.Encode(data)+`"],"id":1,"jsonrpc":"2.0"}`),
&resp,
c.Web3Auth,
); err != nil {
return "", err
}

// Success
if resp.Result != "" {
return resp.Result, nil
}

return "", fmt.Errorf(resp.Error.Message)
}

// send transaction
35 changes: 35 additions & 0 deletions lib/txbuilder/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package txbuilder

import (
"github.com/hanchon/hanchond/lib/requester"
)

type TxBuilder struct {
contracts map[string]Contract
mnemonic string

customGasLimit map[string]uint64
defaultGasLimit uint64

currentNonce map[string]uint64

requester requester.Client
}

func NexTxBuilder(
contracts map[string]Contract,
mnemonic string,
customGasLimit map[string]uint64,
defaultGasLimit uint64,
requester requester.Client,
) *TxBuilder {
return &TxBuilder{
contracts: contracts,
mnemonic: mnemonic,
customGasLimit: customGasLimit,
defaultGasLimit: defaultGasLimit,
currentNonce: map[string]uint64{},

requester: requester,
}
}
20 changes: 20 additions & 0 deletions lib/txbuilder/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package txbuilder

import (
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)

type Contract struct {
address common.Address
ABI abi.ABI
}

func NewContract(address string, abi abi.ABI) Contract {
return Contract{
address: common.HexToAddress(address),
ABI: abi,
}
}
23 changes: 23 additions & 0 deletions lib/txbuilder/contract_interaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package txbuilder

import "math/big"

func (t *TxBuilder) InteractWithContract(
contractName string,
accountID int,
value *big.Int,
message string,
args ...interface{},
) (string, error) {
wallet, account, err := WalletFromMnemonicWithAccountID(t.mnemonic, accountID)
if err != nil {
return "", err
}

privateKey, err := wallet.PrivateKey(account)
if err != nil {
return "", err
}

return t.SendTransaction(contractName, account.Address, privateKey, value, message, args...)
}
9 changes: 9 additions & 0 deletions lib/txbuilder/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package txbuilder

func (t *TxBuilder) GetGasLimit(method string) uint64 {
v, ok := t.customGasLimit[method]
if ok {
return v
}
return t.defaultGasLimit
}
68 changes: 68 additions & 0 deletions lib/txbuilder/transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package txbuilder

import (
"crypto/ecdsa"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

func (t *TxBuilder) SendTransaction(contractName string, address common.Address, privateKey *ecdsa.PrivateKey, value *big.Int, message string, args ...interface{}) (string, error) {
var contractABI abi.ABI
var contractAddress common.Address
var err error

if v, ok := t.contracts[contractName]; ok {
contractABI = v.ABI
contractAddress = v.address
} else {
return "", fmt.Errorf("invalid contract name")
}

v, ok := t.currentNonce[address.Hex()]
nonce := uint64(0)
if ok {
nonce = v
} else {
nonce, err = t.requester.GetNonce(address.Hex())
if err != nil {
return "", err
}
}

gasLimit := t.GetGasLimit(message)
gasPrice, err := t.requester.GasPrice()
if err != nil {
return "", err
}

var data []byte
data, err = contractABI.Pack(message, args...)
if err != nil {
return "", err
}

tx := types.NewTransaction(nonce, contractAddress, value, gasLimit, gasPrice, data)

chainID, err := t.requester.ChanID()
if err != nil {
return "", err
}

signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
return "", err
}

txhash, err := t.requester.BroadcastTx(signedTx)
if err != nil {
return "", err
}

t.currentNonce[address.Hex()] = nonce + 1

return txhash, nil
}
Loading

0 comments on commit 807031f

Please sign in to comment.