diff --git a/README.md b/README.md index 9ba25451..fc688643 100644 --- a/README.md +++ b/README.md @@ -1,189 +1,154 @@ -# w3 +# `w3`: Enhanced Ethereum Integration for Go [![Go Reference](https://pkg.go.dev/badge/github.com/lmittmann/w3.svg)](https://pkg.go.dev/github.com/lmittmann/w3) [![Go Report Card](https://goreportcard.com/badge/github.com/lmittmann/w3)](https://goreportcard.com/report/github.com/lmittmann/w3) [![Coverage Status](https://coveralls.io/repos/github/lmittmann/w3/badge.svg?branch=main)](https://coveralls.io/github/lmittmann/w3?branch=main) [![Latest Release](https://img.shields.io/github/v/release/lmittmann/w3)](https://github.com/lmittmann/w3/releases) +W3 Gopher -W3 Gopher +`w3` is your toolbelt for integrating with Ethereum in Go. Closely linked to `go‑ethereum`, it provides an ergonomic wrapper for working with **RPC**, **ABI's**, and the **EVM**. -Package `w3` implements a blazing fast and modular Ethereum JSON RPC client with -first-class ABI support. - -* **Batch request** support significantly reduces the duration of requests to - both remote and local endpoints. -* **ABI** bindings are specified for individual functions using Solidity syntax. - No need for `abigen` and ABI JSON files. -* **Modular** API allows to create custom RPC method integrations that can be - used alongside the methods implemented by the package. - -`w3` is closely linked to [go-ethereum](https://github.com/ethereum/go-ethereum) -and uses a variety of its types, such as [`common.Address`](https://pkg.go.dev/github.com/ethereum/go-ethereum/common#Address) -or [`types.Transaction`](https://pkg.go.dev/github.com/ethereum/go-ethereum/core/types#Transaction). - -Batch requests with `w3` are up to **85x faster** than sequential requests with -`go-ethereum/ethclient`. - -
-Benchmarks -
-name               ethclient time/op  w3 time/op  delta
-Call_BalanceNonce  78.3ms ± 2%        39.0ms ± 1%  -50.15%  (p=0.000 n=23+22)
-Call_Balance100     3.90s ± 5%         0.05s ± 2%  -98.84%  (p=0.000 n=20+24)
-Call_BalanceOf100   3.99s ± 3%         0.05s ± 2%  -98.73%  (p=0.000 n=22+23)
-Call_Block100       6.89s ± 7%         1.94s ±11%  -71.77%  (p=0.000 n=24+23)
-
-
- -## Install ``` go get github.com/lmittmann/w3 ``` -## Getting Started - -> **Note** -> Check out the [examples](https://github.com/lmittmann/w3/tree/main/examples)! +## At a Glance -Connect to an RPC endpoint via HTTP, WebSocket, or IPC using [`Dial`](https://pkg.go.dev/github.com/lmittmann/w3#Dial) -or [`MustDial`](https://pkg.go.dev/github.com/lmittmann/w3#MustDial). +* Use `w3.Client` to connect to an RPC endpoint. The client features batch request support for up to **80x faster requests** and easy extendibility. [learn more ↗](#rpc-client) +* Use `w3vm.VM` to simulate EVM execution with optional tracing and Mainnet state forking, or test Smart Contracts. [learn more ↗](#vm) +* Use `w3.Func` and `w3.Event` to create ABI bindings from Solidity function and event signatures. [learn more ↗](#abi-bindings) +* Use `w3.A`, `w3.H`, and many other utility functions to parse addresses, hashes, and other common types from strings. [learn more ↗](#utils) -```go -// Connect (or panic on error) -client := w3.MustDial("https://rpc.ankr.com/eth") -defer client.Close() -``` +## Getting Started -## Batch Requests +### RPC Client -Batch request support in the [`Client`](https://pkg.go.dev/github.com/lmittmann/w3#Client) -allows to send multiple RPC requests in a single HTTP request. The speed gains -to remote endpoints are huge. Fetching 100 blocks in a single batch request -with `w3` is ~80x faster compared to sequential requests with `ethclient`. +[`w3.Client`](https://pkg.go.dev/github.com/lmittmann/w3#Client) is a batch request focused RPC client that can be used to connect to an Ethereum node via HTTP, WebSocket, or IPC. Its modular API allows to create custom RPC method integrations that can be used alongside the common methods implemented by this package. -Example: Request the nonce and balance of an address in a single request +**Example:** Batch Request ([Playground](https://pkg.go.dev/github.com/lmittmann/w3#example-Client)) ```go -var ( - addr = w3.A("0x000000000000000000000000000000000000c0Fe") +// 1. Connect to an RPC endpoint +client, err := w3.Dial("https://rpc.ankr.com/eth") +if err != nil { + // handle error +} +defer client.Close() - nonce uint64 - balance big.Int -) -err := client.Call( - eth.Nonce(addr, nil).Returns(&nonce), - eth.Balance(addr, nil).Returns(&balance), +// 2. Make a batch request +var ( + balance big.Int + nonce uint64 ) +if err := client.Call( + eth.Balance(addr, nil).Returns(&balance), + eth.Nonce(addr, nil).Returns(&nonce), +); err != nil { + // handle error +} ``` +> [!NOTE] +> #### why send batch requests? +> Most of the time you need to call multiple RPC methods to get the data you need. When you make separate requests per RPC call you need a single round trip to the server for each call. This can be slow, especially for remote endpoints. Batching multiple RPC calls into a single request only requires a single round trip, and speeds up RPC calls significantly. -## ABI Bindings +#### Learn More +* List of supported [**RPC methods**](#rpc-methods) +* Learn how to create [**custom RPC method bindings**](#custom-rpc-method-bindings) -ABI bindings in `w3` are specified for individual functions using Solidity -syntax and are usable for any contract that supports that function. +### VM -Example: ABI binding for the ERC20-function `balanceOf` +[`w3vm.VM`](https://pkg.go.dev/github.com/lmittmann/w3/w3vm#VM) is a high-level EVM environment with a simple but powerful API to simulate EVM execution, test Smart Contracts, or trace transactions. It supports Mainnet state forking via RPC and state caching for faster testing. -```go -funcBalanceOf := w3.MustNewFunc("balanceOf(address)", "uint256") -``` - -A [`Func`](https://pkg.go.dev/github.com/lmittmann/w3#Func) can be used to - -* encode arguments to the contracts input data ([`Func.EncodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.EncodeArgs)), -* decode arguments from the contracts input data ([`Func.DecodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeArgs)), and -* decode returns form the contracts output data ([`Func.DecodeReturns`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeReturns)). - -### Reading Contracts - -[`Func`](https://pkg.go.dev/github.com/lmittmann/w3#Func)'s can be used with -[`eth.CallFunc`](https://pkg.go.dev/github.com/lmittmann/w3/module/eth#CallFunc) -in the client to read contract data. +**Example:** Simulate an Uniswap v3 swap ([Playground](https://pkg.go.dev/github.com/lmittmann/w3/w3vm#example-VM)) ```go -var ( - weth9 = w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") - dai = w3.A("0x6B175474E89094C44Da98b954EedeAC495271d0F") - - weth9Balance big.Int - daiBalance big.Int -) - -err := client.Call( - eth.CallFunc(weth9, funcBalanceOf, addr).Returns(&weth9Balance), - eth.CallFunc(dai, funcBalanceOf, addr).Returns(&daiBalance), +// 1. Create a VM that forks the Mainnet state from the latest block, +// disables the base fee, and has a fake WETH balance and approval for the router +vm, err := w3vm.New( + w3vm.WithFork(client, nil), + w3vm.WithNoBaseFee(), + w3vm.WithState(w3types.State{ + addrWETH: {Storage: w3types.Storage{ + w3vm.WETHBalanceSlot(addrEOA): common.BigToHash(w3.I("1 ether")), + w3vm.WETHAllowanceSlot(addrEOA, addrRouter): common.BigToHash(w3.I("1 ether")), + }}, + }), ) +if err != nil { + // handle error +} + +// 2. Simulate a Uniswap v3 swap +receipt, err := vm.Apply(&w3types.Message{ + From: addrEOA, + To: &addrRouter, + Func: funcExactInput, + Args: []any{&ExactInputParams{ + Path: encodePath(addrWETH, 500, addrUNI), + Recipient: addrEOA, + Deadline: big.NewInt(time.Now().Unix()), + AmountIn: w3.I("1 ether"), + AmountOutMinimum: w3.Big0, + }}, +}) +if err != nil { + // handle error +} + +// 3. Decode output amount +var amountOut *big.Int +if err := receipt.DecodeReturns(&amountOut); err != nil { + // handle error +} ``` -### Writing Contracts - -Sending a transaction to a contract requires three steps. - -1. Encode the transaction input data using [`Func.EncodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.EncodeArgs). - -```go -var funcTransfer = w3.MustNewFunc("transfer(address,uint256)", "bool") +### ABI Bindings -input, err := funcTransfer.EncodeArgs(w3.A("0x…"), w3.I("1 ether")) -``` +ABI bindings in `w3` are specified for individual functions using Solidity syntax and are usable for any contract that supports that function. -2. Create a signed transaction to the contract using [go-ethereum/types](https://github.com/ethereum/go-ethereum). +**Example:** ABI binding for the ERC20 `balanceOf` function ([Playground](https://pkg.go.dev/github.com/lmittmann/w3#example-NewFunc-BalanceOf)) ```go -signer := types.LatestSigner(params.MainnetChainConfig) -tx := types.MustSignNewTx(privKey, signer, &types.DynamicFeeTx{ - To: w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), - Nonce: 0, - Data: input, - Gas: 75000, - GasFeeCap: w3.I("100 gwei"), - GasTipCap: w3.I("1 gwei"), -}) +funcBalanceOf := w3.MustNewFunc("balanceOf(address)", "uint256") ``` -3. Send the signed transaction. +**Example:** ABI binding for the Uniswap v4 `swap` function ([Playground](https://pkg.go.dev/github.com/lmittmann/w3#example-NewFunc-UniswapV4Swap)) ```go -var txHash common.Hash -err := client.Call( - eth.SendTx(tx).Returns(&txHash), -) +funcSwap := w3.MustNewFunc(`swap( + (address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, + (bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96) params, + bytes hookData +)`, "int256 delta") ``` +A [`Func`](https://pkg.go.dev/github.com/lmittmann/w3#Func) can be used to -## Custom RPC Methods - -Custom RPC methods can be called with the `w3` client by creating a -[`w3types.Caller`](https://pkg.go.dev/github.com/lmittmann/w3/w3types#Caller) -implementation. -The `w3/module/eth` package can be used as implementation reference. - +* encode arguments to the contracts input data ([`Func.EncodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.EncodeArgs)), +* decode arguments from the contracts input data ([`Func.DecodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeArgs)), and +* decode returns form the contracts output data ([`Func.DecodeReturns`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeReturns)). -## Utils +### Utils -Static addresses, hashes, hex byte slices or `big.Int`'s can be parsed from -strings with the following utility functions. +Static addresses, hashes, bytes or integers can be parsed from (hex-)strings with the following utility functions that panic if the string is not valid. ```go -var ( - addr = w3.A("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") - hash = w3.H("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") - bytes = w3.B("0x27c5342c") - big = w3.I("12.34 ether") -) +addr := w3.A("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") +hash := w3.H("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") +bytes := w3.B("0x27c5342c") +amount := w3.I("12.34 ether") ``` -Note that these functions panic if the string cannot be parsed. Use -[go-ethereum/common](https://pkg.go.dev/github.com/ethereum/go-ethereum/common) -to parse strings that may not be valid instead. +Use [go-ethereum/common](https://pkg.go.dev/github.com/ethereum/go-ethereum/common) to parse strings that may not be valid instead. ## RPC Methods -List of supported RPC methods. +List of supported RPC methods for [`w3.Client`](https://pkg.go.dev/github.com/lmittmann/w3#Client). ### [`eth`](https://pkg.go.dev/github.com/lmittmann/w3/module/eth) @@ -242,3 +207,55 @@ List of supported RPC methods. | Package | Description | :----------------------------------------------------------------------- | :----------- | [github.com/lmittmann/flashbots](https://github.com/lmittmann/flashbots) | Package `flashbots` implements RPC API bindings for the Flashbots relay and mev-geth. + + +## Custom RPC Method Bindings + +Custom RPC method bindings can be created by implementing the [`w3types.RPCCaller`](https://pkg.go.dev/github.com/lmittmann/w3/w3types#RPCCaller) interface. + +**Example:** RPC binding for the Otterscan `ots_getTransactionBySenderAndNonce` method ([Playground](https://pkg.go.dev/github.com/lmittmann/w3/w3types#example-RPCCaller-GetTransactionBySenderAndNonce)) + +```go +// TxBySenderAndNonceFactory requests the senders transaction hash by the nonce. +func TxBySenderAndNonceFactory(sender common.Address, nonce uint64) w3types.RPCCallerFactory[common.Hash] { + return &getTransactionBySenderAndNonceFactory{ + sender: sender, + nonce: nonce, + } +} + +// getTransactionBySenderAndNonceFactory implements the w3types.RPCCaller and +// w3types.RPCCallerFactory interfaces. It stores the method parameters and +// the the reference to the return value. +type getTransactionBySenderAndNonceFactory struct { + // params + sender common.Address + nonce uint64 + + // returns + returns *common.Hash +} + +// Returns sets the reference to the return value. +func (f *getTransactionBySenderAndNonceFactory) Returns(txHash *common.Hash) w3types.RPCCaller { + f.returns = txHash + return f +} + +// CreateRequest creates a batch request element for the Otterscan getTransactionBySenderAndNonce method. +func (f *getTransactionBySenderAndNonceFactory) CreateRequest() (rpc.BatchElem, error) { + return rpc.BatchElem{ + Method: "ots_getTransactionBySenderAndNonce", + Args: []any{f.sender, f.nonce}, + Result: f.returns, + }, nil +} + +// HandleResponse handles the response of the Otterscan getTransactionBySenderAndNonce method. +func (f *getTransactionBySenderAndNonceFactory) HandleResponse(elem rpc.BatchElem) error { + if err := elem.Error; err != nil { + return err + } + return nil +} +``` diff --git a/client_test.go b/client_test.go index 43f7ddf3..2b3b3c52 100644 --- a/client_test.go +++ b/client_test.go @@ -14,7 +14,9 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/google/go-cmp/cmp" "github.com/lmittmann/w3" @@ -34,20 +36,32 @@ var ( `< [{"jsonrpc":"2.0","id":1,"result":"0x1"},{"jsonrpc":"2.0","id":2,"result":"0x1"}]` ) -func ExampleDial() { +func ExampleClient() { + addr := w3.A("0x0000000000000000000000000000000000000000") + + // 1. Connect to an RPC endpoint client, err := w3.Dial("https://rpc.ankr.com/eth") if err != nil { - // ... + // handle error } defer client.Close() -} -func ExampleMustDial() { - client := w3.MustDial("https://rpc.ankr.com/eth") - defer client.Close() + // 2. Make a batch request + var ( + balance big.Int + nonce uint64 + ) + if err := client.Call( + eth.Balance(addr, nil).Returns(&balance), + eth.Nonce(addr, nil).Returns(&nonce), + ); err != nil { + // handle error + } + + fmt.Printf("balance: %s\nnonce: %d\n", w3.FromWei(&balance, 18), nonce) } -func ExampleClient_Call() { +func ExampleClient_Call_balanceOf() { // Connect to RPC endpoint (or panic on error) and // close the connection when you are done. client := w3.MustDial("https://rpc.ankr.com/eth") @@ -103,6 +117,42 @@ func ExampleClient_Call_nonceAndBalance() { fmt.Printf("%s: Nonce: %d, Balance: ♦%s\n", addr, nonce, w3.FromWei(&balance, 18)) } +func ExampleClient_Call_sendERC20transferTx() { + client := w3.MustDial("https://rpc.ankr.com/eth") + defer client.Close() + + var ( + weth9 = w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") + receiver = w3.A("0x000000000000000000000000000000000000c0Fe") + eoaPrv, _ = crypto.GenerateKey() + ) + + funcTransfer := w3.MustNewFunc("transfer(address receiver, uint256 amount)", "bool") + input, err := funcTransfer.EncodeArgs(receiver, w3.I("1 ether")) + if err != nil { + fmt.Printf("Failed to encode args: %v\n", err) + return + } + + signer := types.LatestSigner(params.MainnetChainConfig) + var txHash common.Hash + if err := client.Call( + eth.SendTx(types.MustSignNewTx(eoaPrv, signer, &types.DynamicFeeTx{ + Nonce: 0, + To: &weth9, + Data: input, + GasTipCap: w3.I("1 gwei"), + GasFeeCap: w3.I("100 gwei"), + Gas: 100_000, + })).Returns(&txHash), + ); err != nil { + fmt.Printf("Failed to send tx: %v\n", err) + return + } + + fmt.Printf("Sent tx: %s\n", txHash) +} + func TestClientCall(t *testing.T) { tests := []struct { Buf *bytes.Buffer diff --git a/docs/next.config.js b/docs/next.config.js index e6bfeef8..5817c7f9 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -4,13 +4,14 @@ const withNextra = nextra({ theme: 'nextra-theme-docs', themeConfig: './theme.config.jsx', staticImage: true, - flexsearch: { - codeblocks: false + search: { + codeblocks: true }, defaultShowCopyCode: true }) export default withNextra({ + output: 'export', reactStrictMode: true, images: { unoptimized: true, diff --git a/docs/package.json b/docs/package.json index ea1bc572..ef06d9a7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,18 +2,18 @@ "type": "module", "scripts": { "dev": "next dev", - "build": "next build && next export" + "build": "next build" }, "dependencies": { - "next": "^13.4.19", - "nextra": "^2.12.3", - "nextra-theme-docs": "^2.12.3", + "next": "^14.1.4", + "nextra": "3.0.0-alpha.22", + "nextra-theme-docs": "3.0.0-alpha.22", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "autoprefixer": "^10.4.15", - "postcss": "^8.4.29", - "tailwindcss": "^3.3.3" + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3" } } diff --git a/docs/pages/_meta.js b/docs/pages/_meta.js new file mode 100644 index 00000000..757dd35e --- /dev/null +++ b/docs/pages/_meta.js @@ -0,0 +1,22 @@ +export default { + index: { + title: 'Introduction', + theme: { + sidebar: false, + toc: true, + breadcrumb: false, + }, + }, + examples: { + title: 'Examples ↗', + type: 'page', + href: '/examples', + newWindow: true + }, + godoc: { + title: 'GoDoc ↗', + type: 'page', + href: 'https://pkg.go.dev/github.com/lmittmann/w3#section-documentation', + newWindow: true + }, +} diff --git a/docs/pages/_meta.json b/docs/pages/_meta.json deleted file mode 100644 index a76b6231..00000000 --- a/docs/pages/_meta.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "examples": { - "title": "Examples ↗", - "type": "page", - "href": "/examples", - "newWindow": true - }, - "godoc": { - "title": "GoDoc ↗", - "type": "page", - "href": "https://pkg.go.dev/github.com/lmittmann/w3#section-documentation", - "newWindow": true - }, - "index": { - "type": "page", - "display": "hidden" - } -} diff --git a/docs/pages/abi.mdx b/docs/pages/abi.mdx deleted file mode 100644 index b8099cae..00000000 --- a/docs/pages/abi.mdx +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: ABI Bindings -draft: true ---- - -# ABI - -ABI bindings allow the encoding and decoding of Smart Contract function calls or the decoding of events. -In `w3` ABI bindings are defined for individual functions or events at runtime using Solidity syntax. - -* **Easy to write:** Creating an ABI binding only requires the Solidity function signature. No need - to firstly generate an ABI json file using `solc` and secondly generate ABI bindings using `abigen`. -* **Flexible:** ABI bindings for a function or event can be used with any Smart Contract. No need to - generate overlapping bindings for multiple Smart Contracts. - - -## Functions - -Function ABI bindings can be defined using -* `func NewFunc(signature, returns string) (*Func, error)`, or -* `func MustNewFunc(signature, returns string) *Func` which panics on error. - -### Examples - -#### ERC20 `transfer` - -The ERC20 `transfer` function can be defined as follows: -```go -funcTransfer := w3.MustNewFunc("transfer(address,uint256)", "bool") -``` - -Alternatively the function arguments and returns can also be named. Note, that Solidity type aliases -are also supported (e.g. `uint` for `uint256`): -```go -funcTransfer := w3.MustNewFunc("transfer(address to, uint amount)", "bool success") -``` - -#### Tuples - -The UniSwap V3 [`IQuoterV2.sol`](https://github.com/Uniswap/v3-periphery/blob/main/contracts/interfaces/IQuoterV2.sol#L27-L53) -uses a tuple as argument for e.g. `quoteExactInputSingle`, which can be defined as follows: - -```go -funcQuoteExactInputSingle := w3.MustNewFunc( - `quoteExactInputSingle(( - address tokenIn, - address tokenOut, - uint256 amountIn, - uint24 fee, - uint160 sqrtPriceLimitX96 - ) params)`, - `uint256 amountOut, - uint160 sqrtPriceX96After, - uint32 initializedTicksCrossed, - uint256 gasEstimate`, -) -``` - -Note, that the Solidity struct arguments are wrapped in parenthesis and names are mandatory. - -### `EncodeArgs` - -### `DecodeArgs` and `DecodeReturns` - - - - -## Events - -Event ABI bindings can be defined using -* `func NewEvent(signature string) (*Event, error)`, or -* `func MustNewEvent(signature string) *Event` which panics on error. - -### Examples - -#### ERC20 `Transfer` - -```go -evtTransfer := w3.MustNewEvent("Transfer(address indexed from, address indexed to, uint256 value)") -``` diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index a9b6708d..b7c4b25e 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -1,12 +1,11 @@ --- -title: Introduction -theme: - breadcrumb: false +description: 'w3: Enhanced Ethereum Integration for Go' --- import Image from 'next/image' +import { Callout } from 'nextra/components' -# w3 +# `w3`: Enhanced Ethereum Integration for Go
@@ -23,199 +22,169 @@ import Image from 'next/image'
-Hello +Hello -Package `w3` implements a blazing fast and modular Ethereum JSON RPC client with -first-class ABI support. +`w3` is your toolbelt for integrating with Ethereum in Go. Closely linked to `go‑ethereum`, it provides an ergonomic wrapper for working with **RPC**, **ABI's**, and the **EVM**. -* **Batch request** support significantly reduces the duration of requests to - both remote and local endpoints. -* **ABI** bindings are specified for individual functions using Solidity syntax. - No need for `abigen` and ABI JSON files. -* **Modular** API allows to create custom RPC method integrations that can be - used alongside the methods implemented by the package. - -`w3` is closely linked to [go-ethereum](https://github.com/ethereum/go-ethereum) -and uses a variety of its types, such as [`common.Address`](https://pkg.go.dev/github.com/ethereum/go-ethereum/common#Address) -or [`types.Transaction`](https://pkg.go.dev/github.com/ethereum/go-ethereum/core/types#Transaction). - -Batch requests with `w3` are up to **85x faster** than sequential requests with -`go-ethereum/ethclient`. - -
-Benchmarks -
-name               ethclient time/op  w3 time/op  delta
-Call_BalanceNonce  78.3ms ± 2%        39.0ms ± 1%  -50.15%  (p=0.000 n=23+22)
-Call_Balance100     3.90s ± 5%         0.05s ± 2%  -98.84%  (p=0.000 n=20+24)
-Call_BalanceOf100   3.99s ± 3%         0.05s ± 2%  -98.73%  (p=0.000 n=22+23)
-Call_Block100       6.89s ± 7%         1.94s ±11%  -71.77%  (p=0.000 n=24+23)
-
-
- -## Install ``` go get github.com/lmittmann/w3 ``` -## Getting Started +## At a Glance -> **Note** -> Check out the [examples](examples/)! +* Use `w3.Client` to connect to an RPC endpoint. The client features batch request support for up to **80x faster requests** and easy extendibility. [learn more ↗](#rpc-client) +* Use `w3vm.VM` to simulate EVM execution with optional tracing and Mainnet state forking, or test Smart Contracts. [learn more ↗](#vm) +* Use `w3.Func` and `w3.Event` to create ABI bindings from Solidity function and event signatures. [learn more ↗](#abi-bindings) +* Use `w3.A`, `w3.H`, and many other utility functions to parse addresses, hashes, and other common types from strings. [learn more ↗](#utils) -Connect to an RPC endpoint via HTTP, WebSocket, or IPC using [`Dial`](https://pkg.go.dev/github.com/lmittmann/w3#Dial) -or [`MustDial`](https://pkg.go.dev/github.com/lmittmann/w3#MustDial). - -```go -// Connect (or panic on error) -client := w3.MustDial("https://rpc.ankr.com/eth") -defer client.Close() -``` +## Getting Started -## Batch Requests +### RPC Client -Batch request support in the [`Client`](https://pkg.go.dev/github.com/lmittmann/w3#Client) -allows to send multiple RPC requests in a single HTTP request. The speed gains -to remote endpoints are huge. Fetching 100 blocks in a single batch request -with `w3` is ~80x faster compared to sequential requests with `ethclient`. +[`w3.Client`](https://pkg.go.dev/github.com/lmittmann/w3#Client) is a batch request focused RPC client that can be used to connect to an Ethereum node via HTTP, WebSocket, or IPC. Its modular API allows to create custom RPC method integrations that can be used alongside the common methods implemented by this package. -Example: Request the nonce and balance of an address in a single request +**Example:** Batch Request ([Playground](https://pkg.go.dev/github.com/lmittmann/w3#example-Client)) ```go -var ( - addr = w3.A("0x000000000000000000000000000000000000c0Fe") +// 1. Connect to an RPC endpoint +client, err := w3.Dial("https://rpc.ankr.com/eth") +if err != nil { + // handle error +} +defer client.Close() - nonce uint64 - balance big.Int -) -err := client.Call( - eth.Nonce(addr, nil).Returns(&nonce), - eth.Balance(addr, nil).Returns(&balance), +// 2. Make a batch request +var ( + balance big.Int + nonce uint64 ) +if err := client.Call( + eth.Balance(addr, nil).Returns(&balance), + eth.Nonce(addr, nil).Returns(&nonce), +); err != nil { + // handle error +} ``` -## ABI Bindings + + #### why send batch requests? + Most of the time you need to call multiple RPC methods to get the data you need. When you make separate requests per RPC call you need a single round trip to the server for each call. This can be slow, especially for remote endpoints. Batching multiple RPC calls into a single request only requires a single round trip, and speeds up RPC calls significantly. + -ABI bindings in `w3` are specified for individual functions using Solidity -syntax and are usable for any contract that supports that function. +#### Learn More +* List of supported [**RPC methods**](#rpc-methods) +* Learn how to create [**custom RPC method bindings**](#custom-rpc-method-bindings) -Example: ABI binding for the ERC20-function `balanceOf` +### VM -```go -funcBalanceOf := w3.MustNewFunc("balanceOf(address)", "uint256") -``` +[`w3vm.VM`](https://pkg.go.dev/github.com/lmittmann/w3/w3vm#VM) is a high-level EVM environment with a simple but powerful API to simulate EVM execution, test Smart Contracts, or trace transactions. It supports Mainnet state forking via RPC and state caching for faster testing. -A [`Func`](https://pkg.go.dev/github.com/lmittmann/w3#Func) can be used to - -* encode arguments to the contracts input data ([`Func.EncodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.EncodeArgs)), -* decode arguments from the contracts input data ([`Func.DecodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeArgs)), and -* decode returns form the contracts output data ([`Func.DecodeReturns`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeReturns)). - -### Reading Contracts - -[`Func`](https://pkg.go.dev/github.com/lmittmann/w3#Func)'s can be used with -[`eth.CallFunc`](https://pkg.go.dev/github.com/lmittmann/w3/module/eth#CallFunc) -in the client to read contract data. +**Example:** Simulate an Uniswap v3 swap ([Playground](https://pkg.go.dev/github.com/lmittmann/w3/w3vm#example-VM)) ```go -var ( - weth9 = w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") - dai = w3.A("0x6B175474E89094C44Da98b954EedeAC495271d0F") - - weth9Balance big.Int - daiBalance big.Int -) - -err := client.Call( - eth.CallFunc(funcBalanceOf, weth9, addr).Returns(&weth9Balance), - eth.CallFunc(funcBalanceOf, dai, addr).Returns(&daiBalance), +// 1. Create a VM that forks the Mainnet state from the latest block, +// disables the base fee, and has a fake WETH balance and approval for the router +vm, err := w3vm.New( + w3vm.WithFork(client, nil), + w3vm.WithNoBaseFee(), + w3vm.WithState(w3types.State{ + addrWETH: {Storage: w3types.Storage{ + w3vm.WETHBalanceSlot(addrEOA): common.BigToHash(w3.I("1 ether")), + w3vm.WETHAllowanceSlot(addrEOA, addrRouter): common.BigToHash(w3.I("1 ether")), + }}, + }), ) +if err != nil { + // handle error +} + +// 2. Simulate a Uniswap v3 swap +receipt, err := vm.Apply(&w3types.Message{ + From: addrEOA, + To: &addrRouter, + Func: funcExactInput, + Args: []any{&ExactInputParams{ + Path: encodePath(addrWETH, 500, addrUNI), + Recipient: addrEOA, + Deadline: big.NewInt(time.Now().Unix()), + AmountIn: w3.I("1 ether"), + AmountOutMinimum: w3.Big0, + }}, +}) +if err != nil { + // handle error +} + +// 3. Decode output amount +var amountOut *big.Int +if err := receipt.DecodeReturns(&amountOut); err != nil { + // handle error +} ``` -### Writing Contracts +### ABI Bindings -Sending a transaction to a contract requires three steps. +ABI bindings in `w3` are specified for individual functions using Solidity syntax and are usable for any contract that supports that function. -1. Encode the transaction input data using [`Func.EncodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.EncodeArgs). +**Example:** ABI binding for the ERC20 `balanceOf` function ([Playground](https://pkg.go.dev/github.com/lmittmann/w3#example-NewFunc-BalanceOf)) ```go -var funcTransfer = w3.MustNewFunc("transfer(address,uint256)", "bool") - -input, err := funcTransfer.EncodeArgs(w3.A("0x…"), w3.I("1 ether")) -``` - -2. Create a signed transaction to the contract using [go-ethereum/core/types](https://pkg.go.dev/github.com/ethereum/go-ethereum/core/types). - -```go -signer := types.LatestSigner(params.MainnetChainConfig) -tx := types.MustSignNewTx(privKey, signer, &types.DynamicFeeTx{ - To: w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), - Nonce: 0, - Data: input, - Gas: 75000, - GasFeeCap: w3.I("100 gwei"), - GasTipCap: w3.I("1 gwei"), -}) +funcBalanceOf := w3.MustNewFunc("balanceOf(address)", "uint256") ``` -3. Send the signed transaction. +**Example:** ABI binding for the Uniswap v4 `swap` function ([Playground](https://pkg.go.dev/github.com/lmittmann/w3#example-NewFunc-UniswapV4Swap)) ```go -var txHash common.Hash -err := client.Call( - eth.SendTx(tx).Returns(&txHash), -) +funcSwap := w3.MustNewFunc(`swap( + (address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, + (bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96) params, + bytes hookData +)`, "int256 delta") ``` +A [`Func`](https://pkg.go.dev/github.com/lmittmann/w3#Func) can be used to -## Custom RPC Methods - -Custom RPC methods can be called with the `w3` client by creating a -[`core.Caller`](https://pkg.go.dev/github.com/lmittmann/w3/core#Caller) -implementation. -The `w3/module/eth` package can be used as implementation reference. - +* encode arguments to the contracts input data ([`Func.EncodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.EncodeArgs)), +* decode arguments from the contracts input data ([`Func.DecodeArgs`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeArgs)), and +* decode returns form the contracts output data ([`Func.DecodeReturns`](https://pkg.go.dev/github.com/lmittmann/w3#Func.DecodeReturns)). -## Utils +### Utils -Static addresses, hashes, hex byte slices or `big.Int`'s can be parsed from -strings with the following utility functions. +Static addresses, hashes, bytes or integers can be parsed from (hex-)strings with the following utility functions that panic if the string is not valid. ```go -var ( - addr = w3.A("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") - hash = w3.H("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") - bytes = w3.B("0x27c5342c") - big = w3.I("12.34 ether") -) +addr := w3.A("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") +hash := w3.H("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") +bytes := w3.B("0x27c5342c") +amount := w3.I("12.34 ether") ``` -Note that these functions panic if the string cannot be parsed. Use -[go-ethereum/common](https://pkg.go.dev/github.com/ethereum/go-ethereum/common) -to parse strings that may not be valid instead. +Use [go-ethereum/common](https://pkg.go.dev/github.com/ethereum/go-ethereum/common) to parse strings that may not be valid instead. ## RPC Methods -List of supported RPC methods. +List of supported RPC methods for [`w3.Client`](https://pkg.go.dev/github.com/lmittmann/w3#Client). ### [`eth`](https://pkg.go.dev/github.com/lmittmann/w3/module/eth) | Method | Go Code | :---------------------------------------- | :------- | `eth_blockNumber` | `eth.BlockNumber().Returns(blockNumber *big.Int)` -| `eth_call` | `eth.Call(msg *w3types.Message, blockNumber *big.Int, overrides w3types.State).Returns(output *[]byte)`
`eth.CallFunc(fn core.Func, contract common.Address, args ...any).Returns(returns ...any)` +| `eth_call` | `eth.Call(msg *w3types.Message, blockNumber *big.Int, overrides w3types.State).Returns(output *[]byte)`
`eth.CallFunc(contract common.Address, f w3types.Func, args ...any).Returns(returns ...any)` | `eth_chainId` | `eth.ChainID().Returns(chainID *uint64)` | `eth_createAccessList` | `eth.AccessList(msg *w3types.Message, blockNumber *big.Int).Returns(resp *eth.AccessListResponse)` | `eth_estimateGas` | `eth.EstimateGas(msg *w3types.Message, blockNumber *big.Int).Returns(gas *uint64)` | `eth_gasPrice` | `eth.GasPrice().Returns(gasPrice *big.Int)` +| `eth_maxPriorityFeePerGas` | `eth.GasTipCap().Returns(gasTipCap *big.Int)` | `eth_getBalance` | `eth.Balance(addr common.Address, blockNumber *big.Int).Returns(balance *big.Int)` -| `eth_getBlockByHash` | `eth.BlockByHash(hash common.Hash).Returns(block *types.Block)`
`eth.HeaderByHash(hash common.Hash).Returns(header *types.Header)` -| `eth_getBlockByNumber` | `eth.BlockByNumber(number *big.Int).Returns(block *types.Block)`
`eth.HeaderByNumber(number *big.Int).Returns(header *types.Header)` +| `eth_getBlockByHash` | `eth.BlockByHash(hash common.Hash).Returns(block *types.Block)`
`eth.HeaderByHash(hash common.Hash).Returns(header *types.Header)` +| `eth_getBlockByNumber` | `eth.BlockByNumber(number *big.Int).Returns(block *types.Block)`
`eth.HeaderByNumber(number *big.Int).Returns(header *types.Header)` +| `eth_getBlockReceipts` | `eth.BlockReceipts(blockNumber *big.Int).Returns(receipts *types.Receipts)` | `eth_getBlockTransactionCountByHash` | `eth.BlockTxCountByHash(hash common.Hash).Returns(count *uint)` | `eth_getBlockTransactionCountByNumber` | `eth.BlockTxCountByNumber(number *big.Int).Returns(count *uint)` | `eth_getCode` | `eth.Code(addr common.Address, blockNumber *big.Int).Returns(code *[]byte)` @@ -226,7 +195,7 @@ List of supported RPC methods. | `eth_getTransactionByBlockNumberAndIndex` | `eth.TxByBlockNumberAndIndex(blockNumber *big.Int, index uint).Returns(tx *types.Transaction)` | `eth_getTransactionCount` | `eth.Nonce(addr common.Address, blockNumber *big.Int).Returns(nonce *uint)` | `eth_getTransactionReceipt` | `eth.TxReceipt(txHash common.Hash).Returns(receipt *types.Receipt)` -| `eth_sendRawTransaction` | `eth.SendRawTx(rawTx []byte).Returns(hash *common.Hash)`
`eth.SendTx(tx *types.Transaction).Returns(hash *common.Hash)` +| `eth_sendRawTransaction` | `eth.SendRawTx(rawTx []byte).Returns(hash *common.Hash)`
`eth.SendTx(tx *types.Transaction).Returns(hash *common.Hash)` | `eth_getUncleByBlockHashAndIndex` | `eth.UncleByBlockHashAndIndex(hash common.Hash, index uint).Returns(uncle *types.Header)` | `eth_getUncleByBlockNumberAndIndex` | `eth.UncleByBlockNumberAndIndex(number *big.Int, index uint).Returns(uncle *types.Header)` | `eth_getUncleCountByBlockHash` | `eth.UncleCountByBlockHash(hash common.Hash).Returns(count *uint)` @@ -236,8 +205,8 @@ List of supported RPC methods. | Method | Go Code | :----------------------- | :------- -| `debug_traceCall` | `debug.TraceCall(msg *w3types.Message, blockNumber *big.Int, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceCall(msg *w3types.Message, blockNumber *big.Int, overrides w3types.State).Returns(trace *debug.CallTrace)` -| `debug_traceTransaction` | `debug.TraceTx(txHash common.Hash, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceTx(txHash common.Hash, overrides w3types.State).Returns(trace *debug.CallTrace)` +| `debug_traceCall` | `debug.TraceCall(msg *w3types.Message, blockNumber *big.Int, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceCall(msg *w3types.Message, blockNumber *big.Int, overrides w3types.State).Returns(trace *debug.CallTrace)` +| `debug_traceTransaction` | `debug.TraceTx(txHash common.Hash, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceTx(txHash common.Hash, overrides w3types.State).Returns(trace *debug.CallTrace)` ### [`txpool`](https://pkg.go.dev/github.com/lmittmann/w3/module/txpool) @@ -258,3 +227,55 @@ List of supported RPC methods. | Package | Description | :----------------------------------------------------------------------- | :----------- | [github.com/lmittmann/flashbots](https://github.com/lmittmann/flashbots) | Package `flashbots` implements RPC API bindings for the Flashbots relay and mev-geth. + + +## Custom RPC Method Bindings + +Custom RPC method bindings can be created by implementing the [`w3types.RPCCaller`](https://pkg.go.dev/github.com/lmittmann/w3/w3types#RPCCaller) interface. + +**Example:** RPC binding for the Otterscan `ots_getTransactionBySenderAndNonce` method ([Playground](https://pkg.go.dev/github.com/lmittmann/w3/w3types#example-RPCCaller-GetTransactionBySenderAndNonce)) + +```go +// TxBySenderAndNonceFactory requests the senders transaction hash by the nonce. +func TxBySenderAndNonceFactory(sender common.Address, nonce uint64) w3types.RPCCallerFactory[common.Hash] { + return &getTransactionBySenderAndNonceFactory{ + sender: sender, + nonce: nonce, + } +} + +// getTransactionBySenderAndNonceFactory implements the w3types.RPCCaller and +// w3types.RPCCallerFactory interfaces. It stores the method parameters and +// the the reference to the return value. +type getTransactionBySenderAndNonceFactory struct { + // params + sender common.Address + nonce uint64 + + // returns + returns *common.Hash +} + +// Returns sets the reference to the return value. +func (f *getTransactionBySenderAndNonceFactory) Returns(txHash *common.Hash) w3types.RPCCaller { + f.returns = txHash + return f +} + +// CreateRequest creates a batch request element for the Otterscan getTransactionBySenderAndNonce method. +func (f *getTransactionBySenderAndNonceFactory) CreateRequest() (rpc.BatchElem, error) { + return rpc.BatchElem{ + Method: "ots_getTransactionBySenderAndNonce", + Args: []any{f.sender, f.nonce}, + Result: f.returns, + }, nil +} + +// HandleResponse handles the response of the Otterscan getTransactionBySenderAndNonce method. +func (f *getTransactionBySenderAndNonceFactory) HandleResponse(elem rpc.BatchElem) error { + if err := elem.Error; err != nil { + return err + } + return nil +} +``` diff --git a/docs/pages/rpc.mdx b/docs/pages/rpc.mdx deleted file mode 100644 index 52fd2d25..00000000 --- a/docs/pages/rpc.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: RPC -draft: true ---- - -# RPC - -Coming soon... diff --git a/docs/pages/style.css b/docs/pages/style.css index 9230c537..0ef07e76 100644 --- a/docs/pages/style.css +++ b/docs/pages/style.css @@ -4,7 +4,7 @@ @layer base { main { - @apply dark:text-neutral-400; + @apply dark:text-neutral-300; } main strong, diff --git a/docs/pages/vm.mdx b/docs/pages/vm.mdx deleted file mode 100644 index 005d22b6..00000000 --- a/docs/pages/vm.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: VM -draft: true ---- - -# VM - -Coming soon... diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 00000000..4f23aa8f Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/theme.config.jsx b/docs/theme.config.jsx index 8c990048..7e717517 100644 --- a/docs/theme.config.jsx +++ b/docs/theme.config.jsx @@ -1,5 +1,5 @@ -import { useRouter } from 'next/router' import Image from 'next/image' +import { useConfig } from 'nextra-theme-docs' export default { logo: <> @@ -8,12 +8,19 @@ export default { w3 , - banner: { - text: '🚧 This site is currently under construction 🚧' - }, - useNextSeoProps() { - const { pathname } = useRouter() - return { titleTemplate: pathname === '/' ? 'w3' : '%s – w3' } + head: () => { + const { frontMatter } = useConfig() + const title = frontMatter.title ? `${frontMatter.title} – w3` : 'w3' + return ( + <> + {title} + + + + ) }, footer: { component: null, @@ -21,15 +28,12 @@ export default { project: { link: 'https://github.com/lmittmann/w3', }, - editLink: { - text: 'Edit this page on GitHub' - }, feedback: { content: null, }, docsRepositoryBase: 'https://github.com/lmittmann/w3/blob/main/docs/pages', - primaryHue: { - dark: 189, - light: 191 - } + color: { + hue: 189, + saturation: 100, + }, } diff --git a/func_test.go b/func_test.go index 286217c4..2a97208b 100644 --- a/func_test.go +++ b/func_test.go @@ -16,7 +16,7 @@ import ( "github.com/lmittmann/w3/w3types" ) -func ExampleNewFunc() { +func ExampleNewFunc_balanceOf() { // ABI binding to the balanceOf function of an ERC20 Token. funcBalanceOf, _ := w3.NewFunc("balanceOf(address)", "uint256") @@ -48,6 +48,50 @@ func ExampleNewFunc() { // balanceOf returns: 49406 } +func ExampleNewFunc_uniswapV4Swap() { + // ABI binding for the Uniswap v4 swap function. + funcSwap, _ := w3.NewFunc(`swap( + (address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) key, + (bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96) params, + bytes hookData + )`, "int256 delta") + + // ABI binding for the PoolKey struct. + type PoolKey struct { + Currency0 common.Address + Currency1 common.Address + Fee *big.Int + TickSpacing *big.Int + Hooks common.Address + } + + // ABI binding for the SwapParams struct. + type SwapParams struct { + ZeroForOne bool + AmountSpecified *big.Int + SqrtPriceLimitX96 *big.Int + } + + // ABI-encode the functions args. + input, _ := funcSwap.EncodeArgs( + &PoolKey{ + Currency0: w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), + Currency1: w3.A("0x6B175474E89094C44Da98b954EedeAC495271d0F"), + Fee: big.NewInt(0), + TickSpacing: big.NewInt(0), + }, + &SwapParams{ + ZeroForOne: false, + AmountSpecified: big.NewInt(0), + SqrtPriceLimitX96: big.NewInt(0), + }, + []byte{}, + ) + fmt.Printf("swap input: 0x%x\n", input) + // Output: + // swap input: 0xf3cd914c000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000 +} + func TestNewFunc(t *testing.T) { tests := []struct { Signature string diff --git a/w3types/interfaces_test.go b/w3types/interfaces_test.go new file mode 100644 index 00000000..398e784c --- /dev/null +++ b/w3types/interfaces_test.go @@ -0,0 +1,78 @@ +package w3types_test + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" + "github.com/lmittmann/w3/w3types" +) + +// TxBySenderAndNonceFactory requests the senders transaction hash by the nonce. +func TxBySenderAndNonceFactory(sender common.Address, nonce uint64) w3types.RPCCallerFactory[common.Hash] { + return &getTransactionBySenderAndNonceFactory{ + sender: sender, + nonce: nonce, + } +} + +// getTransactionBySenderAndNonceFactory implements the w3types.RPCCaller and +// w3types.RPCCallerFactory interfaces. It stores the method parameters and +// the the reference to the return value. +type getTransactionBySenderAndNonceFactory struct { + // params + sender common.Address + nonce uint64 + + // returns + returns *common.Hash +} + +// Returns sets the reference to the return value. +// +// Return implements the [w3types.RPCCallerFactory] interface. +func (f *getTransactionBySenderAndNonceFactory) Returns(txHash *common.Hash) w3types.RPCCaller { + f.returns = txHash + return f +} + +// CreateRequest creates a batch request element for the Otterscan getTransactionBySenderAndNonce method. +// +// CreateRequest implements the [w3types.RPCCaller] interface. +func (f *getTransactionBySenderAndNonceFactory) CreateRequest() (rpc.BatchElem, error) { + return rpc.BatchElem{ + Method: "ots_getTransactionBySenderAndNonce", + Args: []any{f.sender, f.nonce}, + Result: f.returns, + }, nil +} + +// HandleResponse handles the response of the Otterscan getTransactionBySenderAndNonce method. +// +// HandleResponse implements the [w3types.RPCCaller] interface. +func (f *getTransactionBySenderAndNonceFactory) HandleResponse(elem rpc.BatchElem) error { + if err := elem.Error; err != nil { + return err + } + return nil +} + +func ExampleRPCCaller_getTransactionBySenderAndNonce() { + client := w3.MustDial("https://docs-demo.quiknode.pro") + defer client.Close() + + addr := w3.A("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") + + var firstTxHash common.Hash + if err := client.Call( + TxBySenderAndNonceFactory(addr, 0).Returns(&firstTxHash), + ); err != nil { + fmt.Printf("Request failed: %v\n", err) + return + } + + fmt.Printf("First Tx Hash: %s\n", firstTxHash) + // Output: + // First Tx Hash: 0x6ff0860e202c61189cb2a3a38286bffd694acbc50577df6cb5a7ff40e21ea074 +} diff --git a/w3vm/vm_test.go b/w3vm/vm_test.go index d5aec9c9..bd9ae55e 100644 --- a/w3vm/vm_test.go +++ b/w3vm/vm_test.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -538,3 +539,114 @@ func BenchmarkTransferWETH9(b *testing.B) { } func ptr[T any](t T) *T { return &t } + +func ExampleVM() { + var ( + addrEOA = w3.A("0x000000000000000000000000000000000000c0Fe") + addrWETH = w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") + addrUNI = w3.A("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984") + addrRouter = w3.A("0xE592427A0AEce92De3Edee1F18E0157C05861564") + + funcExactInput = w3.MustNewFunc(`exactInput( + ( + bytes path, + address recipient, + uint256 deadline, + uint256 amountIn, + uint256 amountOutMinimum + ) params + )`, "uint256 amountOut") + ) + + type ExactInputParams struct { + Path []byte + Recipient common.Address + Deadline *big.Int + AmountIn *big.Int + AmountOutMinimum *big.Int + } + + encodePath := func(tokenA common.Address, fee uint32, tokenB common.Address) []byte { + path := make([]byte, 43) + copy(path, tokenA[:]) + path[20], path[21], path[22] = byte(fee>>16), byte(fee>>8), byte(fee) + copy(path[23:], tokenB[:]) + return path + } + + client, err := w3.Dial("https://rpc.ankr.com/eth") + if err != nil { + // handle error + } + defer client.Close() + + // 1. Create a VM that forks the Mainnet state from the latest block, + // disables the base fee, and has a fake WETH balance and approval for the router + vm, err := w3vm.New( + w3vm.WithFork(client, nil), + w3vm.WithNoBaseFee(), + w3vm.WithState(w3types.State{ + addrWETH: {Storage: map[common.Hash]common.Hash{ + w3vm.WETHBalanceSlot(addrEOA): common.BigToHash(w3.I("1 ether")), + w3vm.WETHAllowanceSlot(addrEOA, addrRouter): common.BigToHash(w3.I("1 ether")), + }}, + }), + ) + if err != nil { + // handle error + } + + // 2. Simulate a UniSwap v3 swap + receipt, err := vm.Apply(&w3types.Message{ + From: addrEOA, + To: &addrRouter, + Func: funcExactInput, + Args: []any{&ExactInputParams{ + Path: encodePath(addrWETH, 500, addrUNI), + Recipient: addrEOA, + Deadline: big.NewInt(time.Now().Unix()), + AmountIn: w3.I("1 ether"), + AmountOutMinimum: w3.Big0, + }}, + }) + if err != nil { + // handle error + } + + // 3. Decode output amount + var amountOut *big.Int + if err := receipt.DecodeReturns(&amountOut); err != nil { + // handle error + } + + fmt.Printf("amount out: %s UNI\n", w3.FromWei(amountOut, 18)) +} + +func ExampleVM_Call() { + client := w3.MustDial("https://rpc.ankr.com/eth") + defer client.Close() + + addrWETH := w3.A("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") + addrEOA := w3.A("0x000000000000000000000000000000000000c0Fe") + + vm, err := w3vm.New( + w3vm.WithFork(client, nil), + w3vm.WithState(w3types.State{ + addrWETH: {Storage: map[common.Hash]common.Hash{ + w3vm.WETHBalanceSlot(addrEOA): common.BigToHash(w3.I("1 ether")), + }}, + }), + ) + if err != nil { + // handle error + } + + balanceOf := w3.MustNewFunc("balanceOf(address)", "uint256") + var balance *big.Int + if err := vm.CallFunc(addrWETH, balanceOf, addrEOA).Returns(&balance); err != nil { + // handle error + } + fmt.Printf("%s: Balance: %s WETH\n", addrEOA, w3.FromWei(balance, 18)) + // Output: + // 0x000000000000000000000000000000000000c0Fe: Balance: 1 WETH +}