diff --git a/README.md b/README.md index 26e124a..654e211 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![Run Tests](https://github.com/defiweb/go-eth/actions/workflows/test.yml/badge.svg)](https://github.com/defiweb/go-eth/actions/workflows/test.yml) +**README IS OUTDATED ON THIS BRANCH** + # go-eth This library is a Go package designed to interact with the Ethereum blockchain. This package provides robust tools for diff --git a/abi/encode.go b/abi/encode.go index 52566ea..8cb57ca 100644 --- a/abi/encode.go +++ b/abi/encode.go @@ -207,7 +207,7 @@ func encodeInt(v *big.Int, size int) (Words, error) { if err := x.SetBigInt(v); err != nil { return nil, err } - if err := w.SetBytesPadLeft(x.Bytes()); err != nil { + if err := x.FillBytes(w[:]); err != nil { return nil, err } return Words{w}, nil @@ -224,7 +224,7 @@ func encodeUint(v *big.Int, size int) (Words, error) { if err := x.SetBigInt(v); err != nil { return nil, err } - if err := w.SetBytesPadLeft(x.Bytes()); err != nil { + if err := x.FillBytes(w[:]); err != nil { return nil, err } return Words{w}, nil @@ -259,5 +259,5 @@ func writeInt(w *Word, x int) error { if err := i32.SetInt(x); err != nil { return err } - return w.SetBytesPadLeft(i32.Bytes()) + return i32.FillBytes(w[:]) } diff --git a/abi/event.go b/abi/event.go index 6927774..859acd4 100644 --- a/abi/event.go +++ b/abi/event.go @@ -199,7 +199,7 @@ func (e *Event) String() string { } func (e *Event) calculateTopic0() { - e.topic0 = crypto.Keccak256([]byte(e.signature)) + e.topic0 = types.Hash(crypto.Keccak256([]byte(e.signature))) } func (e *Event) generateSignature() { diff --git a/abi/num.go b/abi/num.go index 55ea08c..0aba0c5 100644 --- a/abi/num.go +++ b/abi/num.go @@ -40,11 +40,12 @@ func (i *intX) BitSize() int { return i.size } -// BitLen returns the number of bits required to represent x. +// BitLen returns the number of bits required to represent the integer. func (i *intX) BitLen() int { return signedBitLen(i.val) } +// IsInt returns true if the integer can be represented as an int. func (i *intX) IsInt() bool { if !i.val.IsInt64() { return false @@ -59,6 +60,7 @@ func (i *intX) IsInt() bool { return true } +// Int returns the int64 representation of the integer. func (i *intX) Int() (int, error) { if !i.val.IsInt64() { return 0, fmt.Errorf("abi: int overflow") @@ -73,6 +75,7 @@ func (i *intX) Int() (int, error) { return int(i.val.Int64()), nil } +// Int64 returns the int64 representation of the integer. func (i *intX) Int64() (int64, error) { if !i.val.IsInt64() { return 0, fmt.Errorf("abi: int64 overflow") @@ -80,7 +83,7 @@ func (i *intX) Int64() (int64, error) { return i.val.Int64(), nil } -// BigInt returns the value of the integer as a big integer. +// BigInt returns the *big.Int representation of the integer. func (i *intX) BigInt() *big.Int { return i.val } @@ -95,6 +98,24 @@ func (i *intX) Bytes() []byte { return r } +// FillBytes fills the byte slice r with the value of the integer as a +// big-endian byte slice. If the byte slice is smaller than the integer, +// an error is returned. The byte slice cannot be larger than 256 bits. +// Negative values are two's complement encoded. +func (i *intX) FillBytes(r []byte) error { + bitLen := len(r) * 8 + if bitLen < i.size { + return fmt.Errorf("abi: cannot fill %d-bit integer to %d bytes", i.size, len(r)) + } + if bitLen > 256 { + return fmt.Errorf("abi: cannot fill byte slices larger than 256 bits") + } + x := new(big.Int).Set(i.val).And(i.val, MaxUint[bitLen]) + x.FillBytes(r) + return nil +} + +// SetInt sets the value of the integer to x. func (i *intX) SetInt(x int) error { if bits.Len(uint(x)) > i.size { return fmt.Errorf("abi: cannot set %d-bit integer to %d-bit int", bits.Len(uint(x)), i.size) @@ -103,6 +124,7 @@ func (i *intX) SetInt(x int) error { return nil } +// SetInt64 sets the value of the integer to x. func (i *intX) SetInt64(x int64) error { if bits.Len64(uint64(x)) > i.size { return fmt.Errorf("abi: cannot set %d-bit integer to %d-bit int64", bits.Len64(uint64(x)), i.size) @@ -156,6 +178,7 @@ func newUintX(bitSize int) *uintX { } } +// Uint returns the uint representation of the integer. func (i *uintX) Uint() (int, error) { if !i.val.IsUint64() { return 0, fmt.Errorf("abi: uint overflow") @@ -167,6 +190,7 @@ func (i *uintX) Uint() (int, error) { return int(i.val.Uint64()), nil } +// Uint64 returns the uint64 representation of the integer. func (i *uintX) Uint64() (uint64, error) { if !i.val.IsUint64() { return 0, fmt.Errorf("abi: int64 overflow") @@ -174,7 +198,7 @@ func (i *uintX) Uint64() (uint64, error) { return i.val.Uint64(), nil } -// BigInt returns the value of the integer as a big integer. +// BigInt returns the *big.Int representation of the integer. func (i *uintX) BigInt() *big.Int { return i.val } @@ -188,6 +212,22 @@ func (i *uintX) Bytes() []byte { return r } +// FillBytes fills the byte slice r with the value of the integer as a +// big-endian byte slice. If the byte slice is smaller than the integer, +// an error is returned. The byte slice cannot be larger than 256 bits. +func (i *uintX) FillBytes(r []byte) error { + bitLen := len(r) * 8 + if bitLen < i.size { + return fmt.Errorf("abi: cannot fill %d-bit integer to %d bytes", i.size, len(r)) + } + if bitLen > 256 { + return fmt.Errorf("abi: cannot fill byte slices larger than 256 bits") + } + i.val.FillBytes(r) + return nil +} + +// SetUint sets the value of the integer to x. func (i *uintX) SetUint(x uint) error { if bits.Len(x) > i.size { return fmt.Errorf("abi: cannot set %d-bit integer to %d-bit int", bits.Len(x), i.size) @@ -196,6 +236,7 @@ func (i *uintX) SetUint(x uint) error { return nil } +// SetUint64 sets the value of the integer to x. func (i *uintX) SetUint64(x uint64) error { if bits.Len64(x) > i.size { return fmt.Errorf("abi: cannot set %d-bit integer to %d-bit int64", bits.Len64(x), i.size) diff --git a/abi/num_test.go b/abi/num_test.go index c440445..53ec6ec 100644 --- a/abi/num_test.go +++ b/abi/num_test.go @@ -75,6 +75,29 @@ func TestIntX_Bytes(t *testing.T) { } } +func TestIntX_SetIntUint(t *testing.T) { + tests := []struct { + x uint64 + bitLen int + want bool + }{ + {x: 0, bitLen: 8, want: true}, + {x: 1, bitLen: 8, want: true}, + {x: 255, bitLen: 8, want: true}, + {x: 256, bitLen: 8, want: false}, + {x: 0, bitLen: 32, want: true}, + {x: 1, bitLen: 32, want: true}, + {x: math.MaxUint32, bitLen: 32, want: true}, + {x: math.MaxUint32 + 1, bitLen: 32, want: false}, + {x: math.MaxUint64, bitLen: 64, want: true}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, canSetUint(tt.x, tt.bitLen)) + }) + } +} + func TestIntX_SetBytes(t *testing.T) { tests := []struct { val *intX @@ -146,6 +169,78 @@ func TestIntX_SetBytes(t *testing.T) { } } +func TestIntX_FillBytes(t *testing.T) { + tests := []struct { + val *intX + set *big.Int + want []byte + wantErr bool + }{ + { + val: newIntX(8), + set: big.NewInt(0), + want: []byte{0x00}, + }, + { + val: newIntX(8), + set: big.NewInt(1), + want: []byte{0x01}, + }, + { + val: newIntX(8), + set: big.NewInt(-1), + want: []byte{0xff}, + }, + { + val: newIntX(8), + set: big.NewInt(127), + want: []byte{0x7f}, + }, + { + val: newIntX(8), + set: big.NewInt(-128), + want: []byte{0x80}, + }, + { + val: newIntX(8), + set: big.NewInt(1), + want: []byte{0x00, 0x01}, + }, + { + val: newIntX(8), + set: big.NewInt(-1), + want: []byte{0xff, 0xff}, + }, + // Too small slice + { + val: newIntX(16), + set: big.NewInt(0), + want: []byte{0x00}, + wantErr: true, + }, + // Too large slice + { + val: newIntX(8), + set: big.NewInt(0), + want: make([]byte, 33), + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + require.NoError(t, tt.val.SetBigInt(tt.set)) + res := make([]byte, len(tt.want)) + err := tt.val.FillBytes(res) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + } + }) + } +} + func Test_signedBitLen(t *testing.T) { tests := []struct { arg *big.Int @@ -196,29 +291,6 @@ func Test_canSetInt(t *testing.T) { } } -func TestIntX_SetIntUint(t *testing.T) { - tests := []struct { - x uint64 - bitLen int - want bool - }{ - {x: 0, bitLen: 8, want: true}, - {x: 1, bitLen: 8, want: true}, - {x: 255, bitLen: 8, want: true}, - {x: 256, bitLen: 8, want: false}, - {x: 0, bitLen: 32, want: true}, - {x: 1, bitLen: 32, want: true}, - {x: math.MaxUint32, bitLen: 32, want: true}, - {x: math.MaxUint32 + 1, bitLen: 32, want: false}, - {x: math.MaxUint64, bitLen: 64, want: true}, - } - for n, tt := range tests { - t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - assert.Equal(t, tt.want, canSetUint(tt.x, tt.bitLen)) - }) - } -} - func bigIntStr(s string) *big.Int { i, ok := new(big.Int).SetString(s, 0) if !ok { diff --git a/abi/value_test.go b/abi/value_test.go index 2fe8ad5..83e23bb 100644 --- a/abi/value_test.go +++ b/abi/value_test.go @@ -278,13 +278,13 @@ func TestEncodeABI(t *testing.T) { }, { name: "int8#127", - val: &IntValue{Size: 256}, + val: &IntValue{Size: 8}, arg: big.NewInt(127), want: Words{padL("7f")}, }, { name: "int8#-128", - val: &IntValue{Size: 256}, + val: &IntValue{Size: 8}, arg: big.NewInt(-128), want: Words{padR("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80")}, }, diff --git a/crypto/crypto.go b/crypto/crypto.go index 6c9283d..e40e334 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -1,73 +1,26 @@ +// Package crypto provides default implementations of cryptographic functions. package crypto import ( - "crypto/ecdsa" - "fmt" - - "github.com/defiweb/go-eth/types" + "github.com/defiweb/go-eth/crypto/ecdsa" + "github.com/defiweb/go-eth/crypto/keccak" + "github.com/defiweb/go-eth/crypto/kzg4844" ) -// Signer is an interface for signing data. -type Signer interface { - // SignHash signs a hash. - SignHash(hash types.Hash) (*types.Signature, error) - - // SignMessage signs a message. - SignMessage(data []byte) (*types.Signature, error) - - // SignTransaction signs a transaction. - SignTransaction(tx *types.Transaction) error -} - -// Recoverer is an interface for recovering addresses from signatures. -type Recoverer interface { - // RecoverHash recovers the address from a hash and signature. - RecoverHash(hash types.Hash, sig types.Signature) (*types.Address, error) - - // RecoverMessage recovers the address from a message and signature. - RecoverMessage(data []byte, sig types.Signature) (*types.Address, error) - - // RecoverTransaction recovers the address from a transaction. - RecoverTransaction(tx *types.Transaction) (*types.Address, error) -} - -// AddMessagePrefix adds the Ethereum message prefix to the given data as -// defined in EIP-191. -func AddMessagePrefix(data []byte) []byte { - return []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)) -} - -// ECSigner returns a Signer implementation for ECDSA. -func ECSigner(key *ecdsa.PrivateKey) Signer { return &ecSigner{key} } - -// ECRecoverer is a Recoverer implementation for ECDSA. -var ECRecoverer Recoverer = &ecRecoverer{} - -type ( - ecSigner struct{ key *ecdsa.PrivateKey } - ecRecoverer struct{} +// Default implementations of the crypto functions. Can be overridden to use +// alternative implementations. +var ( + Keccak256 = keccak.Keccak256 + ECPublicKeyToAddress = ecdsa.PublicKeyToAddress + ECPrivateKeyToPublicKey = ecdsa.PrivateKeyToPublicKey + ECSignHash = ecdsa.SignHash + ECRecoverHash = ecdsa.RecoverHash + ECSignMessage = ecdsa.SignMessage + ECRecoverMessage = ecdsa.RecoverMessage + KZGBlobToCommitment = kzg4844.BlobToCommitment + KZGComputeProof = kzg4844.ComputeProof + KZGVerifyProof = kzg4844.VerifyProof + KZGComputeBlobProof = kzg4844.ComputeBlobProof + KZGVerifyBlobProof = kzg4844.VerifyBlobProof + KZGComputeBlobHashV1 = kzg4844.ComputeBlobHashV1 ) - -func (s *ecSigner) SignHash(hash types.Hash) (*types.Signature, error) { - return ecSignHash(s.key, hash) -} - -func (s *ecSigner) SignMessage(data []byte) (*types.Signature, error) { - return ecSignMessage(s.key, data) -} - -func (s *ecSigner) SignTransaction(tx *types.Transaction) error { - return ecSignTransaction(s.key, tx) -} - -func (r *ecRecoverer) RecoverHash(hash types.Hash, sig types.Signature) (*types.Address, error) { - return ecRecoverHash(hash, sig) -} - -func (r *ecRecoverer) RecoverMessage(data []byte, sig types.Signature) (*types.Address, error) { - return ecRecoverMessage(data, sig) -} - -func (r *ecRecoverer) RecoverTransaction(tx *types.Transaction) (*types.Address, error) { - return ecRecoverTransaction(tx) -} diff --git a/crypto/ecdsa.go b/crypto/ecdsa.go deleted file mode 100644 index 032460e..0000000 --- a/crypto/ecdsa.go +++ /dev/null @@ -1,162 +0,0 @@ -package crypto - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "errors" - "fmt" - "math/big" - - "github.com/btcsuite/btcd/btcec/v2" - btcececdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" - - "github.com/defiweb/go-eth/types" -) - -var s256 = btcec.S256() - -// ECPublicKeyToAddress returns the Ethereum address for the given ECDSA public key. -func ECPublicKeyToAddress(pub *ecdsa.PublicKey) (addr types.Address) { - b := Keccak256(elliptic.Marshal(s256, pub.X, pub.Y)[1:]) - copy(addr[:], b[12:]) - return -} - -// ecSignHash signs the given hash with the given private key. -func ecSignHash(key *ecdsa.PrivateKey, hash types.Hash) (*types.Signature, error) { - if key == nil { - return nil, fmt.Errorf("missing private key") - } - privKey, _ := btcec.PrivKeyFromBytes(key.D.Bytes()) - sig, err := btcececdsa.SignCompact(privKey, hash.Bytes(), false) - if err != nil { - return nil, err - } - v := sig[0] - switch v { - case 27, 28: - v -= 27 - } - copy(sig, sig[1:]) - sig[64] = v - return types.SignatureFromBytesPtr(sig), nil -} - -// ecSignMessage signs the given message with the given private key. -func ecSignMessage(key *ecdsa.PrivateKey, data []byte) (*types.Signature, error) { - if key == nil { - return nil, fmt.Errorf("missing private key") - } - sig, err := ecSignHash(key, Keccak256(AddMessagePrefix(data))) - if err != nil { - return nil, err - } - sig.V = new(big.Int).Add(sig.V, big.NewInt(27)) - return sig, nil -} - -// ecSignTransaction signs the given transaction with the given private key. -func ecSignTransaction(key *ecdsa.PrivateKey, tx *types.Transaction) error { - if key == nil { - return fmt.Errorf("missing private key") - } - from := ECPublicKeyToAddress(&key.PublicKey) - if tx.From != nil && *tx.From != from { - return fmt.Errorf("invalid signer address: %s", tx.From) - } - hash, err := signingHash(tx) - if err != nil { - return err - } - sig, err := ecSignHash(key, hash) - if err != nil { - return err - } - sv, sr, ss := sig.V, sig.R, sig.S - switch tx.Type { - case types.LegacyTxType: - if tx.ChainID != nil { - sv = new(big.Int).Add(sv, new(big.Int).SetUint64(*tx.ChainID*2)) - sv = new(big.Int).Add(sv, big.NewInt(35)) - } else { - sv = new(big.Int).Add(sv, big.NewInt(27)) - } - case types.AccessListTxType: - case types.DynamicFeeTxType: - default: - return fmt.Errorf("unsupported transaction type: %d", tx.Type) - } - tx.From = &from - tx.Signature = types.SignatureFromVRSPtr(sv, sr, ss) - return nil -} - -// ecRecoverHash recovers the Ethereum address from the given hash and signature. -func ecRecoverHash(hash types.Hash, sig types.Signature) (*types.Address, error) { - if sig.V.BitLen() > 8 { - return nil, errors.New("invalid signature: V has more than 8 bits") - } - if sig.R.BitLen() > 256 { - return nil, errors.New("invalid signature: R has more than 256 bits") - } - if sig.S.BitLen() > 256 { - return nil, errors.New("invalid signature: S has more than 256 bits") - } - v := byte(sig.V.Uint64()) - switch v { - case 0, 1: - v += 27 - } - rb := sig.R.Bytes() - sb := sig.S.Bytes() - bin := make([]byte, 65) - bin[0] = v - copy(bin[1+(32-len(rb)):], rb) - copy(bin[33+(32-len(sb)):], sb) - pub, _, err := btcececdsa.RecoverCompact(bin, hash.Bytes()) - if err != nil { - return nil, err - } - addr := ECPublicKeyToAddress(pub.ToECDSA()) - return &addr, nil -} - -// ecRecoverMessage recovers the Ethereum address from the given message and signature. -func ecRecoverMessage(data []byte, sig types.Signature) (*types.Address, error) { - sig.V = new(big.Int).Sub(sig.V, big.NewInt(27)) - return ecRecoverHash(Keccak256(AddMessagePrefix(data)), sig) -} - -// ecRecoverTransaction recovers the Ethereum address from the given transaction. -func ecRecoverTransaction(tx *types.Transaction) (*types.Address, error) { - if tx.Signature == nil { - return nil, fmt.Errorf("signature is missing") - } - sig := *tx.Signature - switch tx.Type { - case types.LegacyTxType: - if tx.Signature.V.Cmp(big.NewInt(35)) >= 0 { - x := new(big.Int).Sub(sig.V, big.NewInt(35)) - - // Derive the chain ID from the signature. - chainID := new(big.Int).Div(x, big.NewInt(2)) - if tx.ChainID != nil && *tx.ChainID != chainID.Uint64() { - return nil, fmt.Errorf("invalid chain ID: %d", chainID) - } - - // Derive the recovery byte from the signature. - sig.V = new(big.Int).Add(new(big.Int).Mod(x, big.NewInt(2)), big.NewInt(27)) - } else { - sig.V = new(big.Int).Sub(sig.V, big.NewInt(27)) - } - case types.AccessListTxType: - case types.DynamicFeeTxType: - default: - return nil, fmt.Errorf("unsupported transaction type: %d", tx.Type) - } - hash, err := signingHash(tx) - if err != nil { - return nil, err - } - return ecRecoverHash(hash, sig) -} diff --git a/crypto/ecdsa/ecdsa.go b/crypto/ecdsa/ecdsa.go new file mode 100644 index 0000000..aba0fe8 --- /dev/null +++ b/crypto/ecdsa/ecdsa.go @@ -0,0 +1,163 @@ +// Package ecdsa provides ECDSA cryptographic functionality for Ethereum. +package ecdsa + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "fmt" + "math/big" + + "github.com/btcsuite/btcd/btcec/v2" + btcececdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" + + "github.com/defiweb/go-eth/crypto/keccak" +) + +// PublicKey is an ECDSA public key. +type PublicKey struct { + X *big.Int + Y *big.Int +} + +// PrivateKey is an ECDSA private key. +type PrivateKey struct { + D *big.Int +} + +// Signature is an ECDSA signature. +// +// For most use cases, the [types.Signature] type should be used instead. +type Signature struct { + V *big.Int + R *big.Int + S *big.Int +} + +// Hash is a 32-byte hash. +// +// For most use cases, the [types.Hash] type should be used instead. +type Hash [32]byte + +// Address is a 20-byte Ethereum address. +// +// For most use cases, the [types.Address] type should be used instead. +type Address [20]byte + +var s256 = btcec.S256() + +// AddMessagePrefix adds the Ethereum message prefix to the given data as +// defined in EIP-191. +func AddMessagePrefix(data []byte) []byte { + return []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)) +} + +// GenerateKey generates a new ECDSA private key. +func GenerateKey() (*PrivateKey, error) { + pk, err := ecdsa.GenerateKey(s256, rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ECDSA key: %w", err) + } + return &PrivateKey{D: pk.D}, nil +} + +// PublicKeyToAddress returns the Ethereum address for the given ECDSA +// public key. +func PublicKeyToAddress(publicKey *PublicKey) (addr Address) { + h := keccak.Keccak256(elliptic.Marshal(s256, publicKey.X, publicKey.Y)[1:]) + copy(addr[:], h[12:]) + return +} + +// PrivateKeyToPublicKey converts a private key to a public key. +// If the private key is nil, it returns nil. +func PrivateKeyToPublicKey(privateKey *PrivateKey) *PublicKey { + if privateKey == nil { + return nil + } + privKey, _ := btcec.PrivKeyFromBytes(privateKey.D.Bytes()) + pubKey := privKey.PubKey() + return &PublicKey{ + X: pubKey.X(), + Y: pubKey.Y(), + } +} + +// SignHash signs the given hash with the given private key. +func SignHash(privateKey *PrivateKey, hash Hash) (*Signature, error) { + if privateKey == nil { + return nil, fmt.Errorf("missing private key") + } + privKey, _ := btcec.PrivKeyFromBytes(privateKey.D.Bytes()) + sig, err := btcececdsa.SignCompact(privKey, hash[:], false) + if err != nil { + return nil, err + } + v := sig[0] + switch v { + case 27, 28: + v -= 27 + } + copy(sig, sig[1:]) + sig[64] = v + return &Signature{ + V: new(big.Int).SetBytes(sig[64:]), + R: new(big.Int).SetBytes(sig[:32]), + S: new(big.Int).SetBytes(sig[32:64]), + }, nil +} + +// RecoverHash recovers the Ethereum address from the given hash and +// signature. +func RecoverHash(hash Hash, signature Signature) (*Address, error) { + if signature.V.BitLen() > 8 { + return nil, errors.New("invalid signature: V has more than 8 bits") + } + if signature.R.BitLen() > 256 { + return nil, errors.New("invalid signature: R has more than 256 bits") + } + if signature.S.BitLen() > 256 { + return nil, errors.New("invalid signature: S has more than 256 bits") + } + v := byte(signature.V.Uint64()) + switch v { + case 0, 1: + v += 27 + } + rb := signature.R.Bytes() + sb := signature.S.Bytes() + bin := make([]byte, 65) + bin[0] = v + copy(bin[1+(32-len(rb)):], rb) + copy(bin[33+(32-len(sb)):], sb) + pub, _, err := btcececdsa.RecoverCompact(bin, hash[:]) + if err != nil { + return nil, err + } + addr := PublicKeyToAddress(&PublicKey{ + X: pub.X(), + Y: pub.Y(), + }) + return &addr, nil +} + +// SignMessage signs the given message with the given private key. +func SignMessage(key *PrivateKey, data []byte) (*Signature, error) { + if key == nil { + return nil, fmt.Errorf("missing private key") + } + sig, err := SignHash(key, Hash(keccak.Keccak256(AddMessagePrefix(data)))) + if err != nil { + return nil, err + } + sig.V = new(big.Int).Add(sig.V, big.NewInt(27)) + return sig, nil +} + +// RecoverMessage recovers the Ethereum address from the given message and +// signature. +func RecoverMessage(data []byte, sig Signature) (*Address, error) { + sig.V = new(big.Int).Sub(sig.V, big.NewInt(27)) + return RecoverHash(Hash(keccak.Keccak256(AddMessagePrefix(data))), sig) +} diff --git a/crypto/ecdsa/ecdsa_test.go b/crypto/ecdsa/ecdsa_test.go new file mode 100644 index 0000000..0726578 --- /dev/null +++ b/crypto/ecdsa/ecdsa_test.go @@ -0,0 +1,72 @@ +package ecdsa + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" +) + +func TestSignHash(t *testing.T) { + key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) + hash := Hash{} + copy(hash[:], bytes.Repeat([]byte{0x02}, 32)) + signature, err := SignHash(&PrivateKey{D: key.ToECDSA().D}, hash) + + require.NoError(t, err) + require.NotNil(t, signature) + assert.Equal(t, "0", signature.V.Text(16)) + assert.Equal(t, "97ef30233ead25d10f7bb2bf9eaf571a16f2deb33a75f20819284f0cb8ff3cc1", signature.R.Text(16)) + assert.Equal(t, "4870ca05940199c113b4dc77866f001702691cde269f6835581e7aea1ead2660", signature.S.Text(16)) +} + +func TestSignMessage(t *testing.T) { + key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) + signature, err := SignMessage(&PrivateKey{D: key.ToECDSA().D}, []byte("hello world")) + + require.NoError(t, err) + require.NotNil(t, signature) + assert.Equal(t, "1b", signature.V.Text(16)) + assert.Equal(t, "f2b67e452d18ce781203f10380ea5a2726494162c49c495069cf99118bcf199", signature.R.Text(16)) + assert.Equal(t, "51601fe3219055482c45a14bf616c3e2bc7914c953f438627de2aa541eef61b5", signature.S.Text(16)) +} + +func TestRecoverHash(t *testing.T) { + hash := Hash{} + copy(hash[:], bytes.Repeat([]byte{0x02}, 32)) + signature := Signature{ + V: hexutil.MustHexToBigInt("1b"), + R: hexutil.MustHexToBigInt("97ef30233ead25d10f7bb2bf9eaf571a16f2deb33a75f20819284f0cb8ff3cc1"), + S: hexutil.MustHexToBigInt("4870ca05940199c113b4dc77866f001702691cde269f6835581e7aea1ead2660"), + } + addr, err := RecoverHash(hash, signature) + + require.NoError(t, err) + require.NotNil(t, addr) + assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", hexutil.BytesToHex(addr[:])) +} + +func TestRecoverMessage(t *testing.T) { + signature := Signature{ + V: hexutil.MustHexToBigInt("1b"), + R: hexutil.MustHexToBigInt("f2b67e452d18ce781203f10380ea5a2726494162c49c495069cf99118bcf199"), + S: hexutil.MustHexToBigInt("51601fe3219055482c45a14bf616c3e2bc7914c953f438627de2aa541eef61b5"), + } + addr, err := RecoverMessage([]byte("hello world"), signature) + + require.NoError(t, err) + require.NotNil(t, addr) + assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", hexutil.BytesToHex(addr[:])) +} + +func TestPublicKeyToAddress(t *testing.T) { + key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) + publicKey := key.PubKey() + addr := PublicKeyToAddress(&PublicKey{publicKey.X(), publicKey.Y()}) + + assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", hexutil.BytesToHex(addr[:])) +} diff --git a/crypto/ecdsa_test.go b/crypto/ecdsa_test.go deleted file mode 100644 index 1ea2b71..0000000 --- a/crypto/ecdsa_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package crypto - -import ( - "bytes" - "math/big" - "testing" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/defiweb/go-eth/hexutil" - "github.com/defiweb/go-eth/types" -) - -func Test_ecSignHash(t *testing.T) { - key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) - signature, err := ecSignHash(key.ToECDSA(), types.MustHashFromBytes(bytes.Repeat([]byte{0x02}, 32), types.PadNone)) - - require.NoError(t, err) - require.NotNil(t, signature) - assert.Equal(t, "0", signature.V.Text(16)) - assert.Equal(t, "97ef30233ead25d10f7bb2bf9eaf571a16f2deb33a75f20819284f0cb8ff3cc1", signature.R.Text(16)) - assert.Equal(t, "4870ca05940199c113b4dc77866f001702691cde269f6835581e7aea1ead2660", signature.S.Text(16)) -} - -func Test_ecSignMessage(t *testing.T) { - key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) - signature, err := ecSignMessage(key.ToECDSA(), []byte("hello world")) - - require.NoError(t, err) - require.NotNil(t, signature) - assert.Equal(t, "1b", signature.V.Text(16)) - assert.Equal(t, "f2b67e452d18ce781203f10380ea5a2726494162c49c495069cf99118bcf199", signature.R.Text(16)) - assert.Equal(t, "51601fe3219055482c45a14bf616c3e2bc7914c953f438627de2aa541eef61b5", signature.S.Text(16)) -} - -//nolint:funlen -func Test_ecSignTransaction(t *testing.T) { - t.Run("legacy", func(t *testing.T) { - key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) - tx := (&types.Transaction{}). - SetType(types.LegacyTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)) - err := ecSignTransaction(key.ToECDSA(), tx) - - require.NoError(t, err) - assert.Equal(t, "1b", tx.Signature.V.Text(16)) - assert.Equal(t, "2bfad43ba1b40e7f3ffb6342b1a6eecc700dd344fb0aba543aed5c10fd1a9470", tx.Signature.R.Text(16)) - assert.Equal(t, "615bff48c483d368ed4f6e327a6ddd8831e544d0ca08f1345433e4ed204f8537", tx.Signature.S.Text(16)) - }) - t.Run("legacy-eip155", func(t *testing.T) { - key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) - tx := (&types.Transaction{}). - SetType(types.LegacyTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetChainID(1337) - err := ecSignTransaction(key.ToECDSA(), tx) - - require.NoError(t, err) - assert.Equal(t, "a95", tx.Signature.V.Text(16)) - assert.Equal(t, "14702a15dd7739397f25e3902a0c2bf6989e93888201139aac2c67a8f33a2f3f", tx.Signature.R.Text(16)) - assert.Equal(t, "4a10ba6cf47ace7e3c847e38583f5b1e1c7d8a862f4b43cd74480a03007363f7", tx.Signature.S.Text(16)) - }) - t.Run("access-list", func(t *testing.T) { - key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) - tx := (&types.Transaction{}). - SetType(types.AccessListTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)) - err := ecSignTransaction(key.ToECDSA(), tx) - - require.NoError(t, err) - assert.Equal(t, "1", tx.Signature.V.Text(16)) - assert.Equal(t, "dc1fcd0c6f56eddc8dbe70635690cce521276b8a6e167f8e57e4064db8a5738e", tx.Signature.R.Text(16)) - assert.Equal(t, "2743f261c001ee472c9664258708eaf849fc85623ee337d2018d37fc6f397d8c", tx.Signature.S.Text(16)) - }) - t.Run("dynamic-fee", func(t *testing.T) { - key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) - tx := (&types.Transaction{}). - SetType(types.DynamicFeeTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetMaxFeePerGas(big.NewInt(20000000000)). - SetMaxPriorityFeePerGas(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)) - err := ecSignTransaction(key.ToECDSA(), tx) - - require.NoError(t, err) - assert.Equal(t, "0", tx.Signature.V.Text(16)) - assert.Equal(t, "62072d055f9ceb871a47f2d81aeb5aa34df50c625da16c6d0d57d232fa3cd152", tx.Signature.R.Text(16)) - assert.Equal(t, "57fd88df7c85076f5729493be7e87f51b618a78bc89441ed741bdfdb9d1d5572", tx.Signature.S.Text(16)) - }) -} - -func Test_ecRecoverHash(t *testing.T) { - addr, err := ecRecoverHash( - types.MustHashFromBytes(bytes.Repeat([]byte{0x02}, 32), types.PadNone), - types.SignatureFromVRS( - hexutil.MustHexToBigInt("1b"), - hexutil.MustHexToBigInt("97ef30233ead25d10f7bb2bf9eaf571a16f2deb33a75f20819284f0cb8ff3cc1"), - hexutil.MustHexToBigInt("4870ca05940199c113b4dc77866f001702691cde269f6835581e7aea1ead2660"), - ), - ) - - require.NoError(t, err) - assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) -} - -func Test_ecRecoverMessage(t *testing.T) { - addr, err := ecRecoverMessage( - []byte("hello world"), - types.SignatureFromVRS( - hexutil.MustHexToBigInt("1b"), - hexutil.MustHexToBigInt("f2b67e452d18ce781203f10380ea5a2726494162c49c495069cf99118bcf199"), - hexutil.MustHexToBigInt("51601fe3219055482c45a14bf616c3e2bc7914c953f438627de2aa541eef61b5"), - ), - ) - - require.NoError(t, err) - assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) -} - -func Test_ecRecoverTransaction(t *testing.T) { - t.Run("legacy", func(t *testing.T) { - tx := (&types.Transaction{}). - SetType(types.LegacyTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.SignatureFromVRS( - hexutil.MustHexToBigInt("1b"), - hexutil.MustHexToBigInt("2bfad43ba1b40e7f3ffb6342b1a6eecc700dd344fb0aba543aed5c10fd1a9470"), - hexutil.MustHexToBigInt("615bff48c483d368ed4f6e327a6ddd8831e544d0ca08f1345433e4ed204f8537"), - )) - addr, err := ecRecoverTransaction(tx) - - require.NoError(t, err) - assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) - }) - t.Run("legacy-eip155", func(t *testing.T) { - tx := (&types.Transaction{}). - SetType(types.LegacyTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetChainID(1337). - SetSignature(types.SignatureFromVRS( - hexutil.MustHexToBigInt("a95"), - hexutil.MustHexToBigInt("14702a15dd7739397f25e3902a0c2bf6989e93888201139aac2c67a8f33a2f3f"), - hexutil.MustHexToBigInt("4a10ba6cf47ace7e3c847e38583f5b1e1c7d8a862f4b43cd74480a03007363f7"), - )) - addr, err := ecRecoverTransaction(tx) - - require.NoError(t, err) - assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) - }) - t.Run("access-list", func(t *testing.T) { - tx := (&types.Transaction{}). - SetType(types.AccessListTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.SignatureFromVRS( - hexutil.MustHexToBigInt("1"), - hexutil.MustHexToBigInt("dc1fcd0c6f56eddc8dbe70635690cce521276b8a6e167f8e57e4064db8a5738e"), - hexutil.MustHexToBigInt("2743f261c001ee472c9664258708eaf849fc85623ee337d2018d37fc6f397d8c"), - )) - addr, err := ecRecoverTransaction(tx) - - require.NoError(t, err) - assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) - }) - t.Run("dynamic-fee", func(t *testing.T) { - tx := (&types.Transaction{}). - SetType(types.DynamicFeeTxType). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetMaxFeePerGas(big.NewInt(20000000000)). - SetMaxPriorityFeePerGas(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.SignatureFromVRS( - hexutil.MustHexToBigInt("0"), - hexutil.MustHexToBigInt("62072d055f9ceb871a47f2d81aeb5aa34df50c625da16c6d0d57d232fa3cd152"), - hexutil.MustHexToBigInt("57fd88df7c85076f5729493be7e87f51b618a78bc89441ed741bdfdb9d1d5572"), - )) - addr, err := ecRecoverTransaction(tx) - - require.NoError(t, err) - assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) - }) -} - -func Test_fuzzECSignHash(t *testing.T) { - for i := int64(0); i < 1000; i++ { - // Generate a random key. - prv := make([]byte, 32) - new(big.Int).SetInt64(i + 1).FillBytes(prv) - key, _ := btcec.PrivKeyFromBytes(prv) - - // Generate a random hash. - msg := make([]byte, 32) - new(big.Int).SetInt64(i + 1).FillBytes(msg) - - // Sign the hash. - s, err := ecSignHash(key.ToECDSA(), types.MustHashFromBytes(msg, types.PadNone)) - require.NoError(t, err) - - // Recover the address. - addr, err := ecRecoverHash(types.MustHashFromBytes(msg, types.PadNone), *s) - require.NoError(t, err) - - // Check that the address is correct. - require.Equal(t, ECPublicKeyToAddress(key.PubKey().ToECDSA()), *addr) - } -} - -func Test_fuzzECSignMessage(t *testing.T) { - for i := int64(0); i < 1000; i++ { - // Generate a random key. - prv := make([]byte, 32) - new(big.Int).SetInt64(i + 1).FillBytes(prv) - key, _ := btcec.PrivKeyFromBytes(prv) - - // Generate a random hash. - msg := make([]byte, 32) - new(big.Int).SetInt64(i + 1).FillBytes(msg) - - // Sign the hash. - s, err := ecSignMessage(key.ToECDSA(), msg) - require.NoError(t, err) - - // Recover the address. - addr, err := ecRecoverMessage(msg, *s) - require.NoError(t, err) - - // Check that the address is correct. - require.Equal(t, ECPublicKeyToAddress(key.PubKey().ToECDSA()), *addr) - } -} diff --git a/crypto/keccak.go b/crypto/keccak.go deleted file mode 100644 index 7188f19..0000000 --- a/crypto/keccak.go +++ /dev/null @@ -1,16 +0,0 @@ -package crypto - -import ( - "golang.org/x/crypto/sha3" - - "github.com/defiweb/go-eth/types" -) - -// Keccak256 calculates the Keccak256 hash of the given data. -func Keccak256(data ...[]byte) types.Hash { - h := sha3.NewLegacyKeccak256() - for _, i := range data { - h.Write(i) - } - return types.MustHashFromBytes(h.Sum(nil), types.PadNone) -} diff --git a/crypto/keccak/keccak.go b/crypto/keccak/keccak.go new file mode 100644 index 0000000..7c96523 --- /dev/null +++ b/crypto/keccak/keccak.go @@ -0,0 +1,21 @@ +// Package keccak provides Keccak-256 hashing functionality. +package keccak + +import ( + "golang.org/x/crypto/sha3" +) + +// Hash is a 32-byte Keccak256 hash. +// +// For most use cases, the [types.Hash] type should be used instead. +type Hash [32]byte + +// Keccak256 calculates the Keccak256 hash of the given data. +func Keccak256(data ...[]byte) (h Hash) { + k := sha3.NewLegacyKeccak256() + for _, i := range data { + k.Write(i) + } + copy(h[:], k.Sum(nil)) + return +} diff --git a/crypto/keccak/keccak_test.go b/crypto/keccak/keccak_test.go new file mode 100644 index 0000000..9fc307f --- /dev/null +++ b/crypto/keccak/keccak_test.go @@ -0,0 +1,35 @@ +package keccak + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeccak256(t *testing.T) { + tc := []struct { + data [][]byte + want string + }{ + { + data: [][]byte{[]byte("")}, + want: "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + }, + { + data: [][]byte{[]byte("ab")}, + want: "67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160", + }, + { + data: [][]byte{[]byte("a"), []byte("b")}, + want: "67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160", + }, + } + for n, tt := range tc { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + h := Keccak256(tt.data...) + assert.Equal(t, tt.want, hex.EncodeToString(h[:])) + }) + } +} diff --git a/crypto/keccak_test.go b/crypto/keccak_test.go deleted file mode 100644 index f9db154..0000000 --- a/crypto/keccak_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package crypto - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestKeccak256(t *testing.T) { - tests := []struct { - data [][]byte - want string - }{ - { - data: [][]byte{[]byte("")}, - want: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - }, - { - data: [][]byte{[]byte("ab")}, - want: "0x67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160", - }, - { - data: [][]byte{[]byte("a"), []byte("b")}, - want: "0x67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160", - }, - } - for n, tt := range tests { - t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - assert.Equal(t, tt.want, Keccak256(tt.data...).String()) - }) - } -} diff --git a/crypto/kzg4844/kzg4844.go b/crypto/kzg4844/kzg4844.go new file mode 100644 index 0000000..5fa6b96 --- /dev/null +++ b/crypto/kzg4844/kzg4844.go @@ -0,0 +1,120 @@ +package kzg4844 + +import ( + "crypto/sha256" + "math/big" + "sync" + + kzg4844 "github.com/crate-crypto/go-kzg-4844" +) + +const ( + ScalarLength = 4096 + ScalarSize = 32 + BlobLength = ScalarLength * ScalarSize // 128 KiB + CommitmentLength = 48 + ProofLength = 48 + PointLength = 32 +) + +// BLSModulus is the BLS12-381 scalar field modulus. +var BLSModulus = new(big.Int).SetBytes(kzg4844.BlsModulus[:]) + +// Blob represents a 4844 data blob. +type Blob [BlobLength]byte + +// Commitment is a serialized commitment to a polynomial. +type Commitment [CommitmentLength]byte + +// Proof is a serialized commitment to the quotient polynomial. +type Proof [ProofLength]byte + +// Point is a BLS field element. +type Point [PointLength]byte + +// BlobToCommitment computes the KZG commitment for the given blob. +func BlobToCommitment(blob *Blob) (Commitment, error) { + initContext() + commitment, err := context.BlobToKZGCommitment( + (*kzg4844.Blob)(blob), + 0, + ) + if err != nil { + return Commitment{}, err + } + return (Commitment)(commitment), nil +} + +// ComputeProof computes the KZG proof and claim for the given blob and point. +func ComputeProof(blob *Blob, point Point) (Proof, Point, error) { + initContext() + proof, claim, err := context.ComputeKZGProof( + (*kzg4844.Blob)(blob), + (kzg4844.Scalar)(point), + 0, + ) + if err != nil { + return Proof{}, Point{}, err + } + return (Proof)(proof), (Point)(claim), nil +} + +// VerifyProof verifies the KZG proof for the given commitment, point, claim, +// and proof. +func VerifyProof(commitment Commitment, point Point, claim Point, proof Proof) error { + initContext() + return context.VerifyKZGProof( + (kzg4844.KZGCommitment)(commitment), + (kzg4844.Scalar)(point), + (kzg4844.Scalar)(claim), + (kzg4844.KZGProof)(proof), + ) +} + +// ComputeBlobProof computes the KZG proof for the given blob and commitment. +func ComputeBlobProof(blob *Blob, commitment Commitment) (Proof, error) { + initContext() + proof, err := context.ComputeBlobKZGProof( + (*kzg4844.Blob)(blob), + (kzg4844.KZGCommitment)(commitment), + 0, + ) + if err != nil { + return Proof{}, err + } + return (Proof)(proof), nil +} + +// VerifyBlobProof verifies the KZG proof for the given blob, commitment, and proof. +func VerifyBlobProof(blob *Blob, commitment Commitment, proof Proof) error { + initContext() + return context.VerifyBlobKZGProof( + (*kzg4844.Blob)(blob), + (kzg4844.KZGCommitment)(commitment), + (kzg4844.KZGProof)(proof), + ) +} + +// ComputeBlobHashV1 calculates the 'versioned blob hash' of a commitment. +func ComputeBlobHashV1(commit Commitment) (h [32]byte) { + k := sha256.New() + k.Write(commit[:]) + k.Sum(h[:0]) + h[0] = 0x01 + return +} + +// context holds the necessary configuration needed to create and verify proofs. +var context *kzg4844.Context + +var once sync.Once + +func initContext() { + once.Do(func() { + var err error + context, err = kzg4844.NewContext4096Secure() + if err != nil { + panic(err) + } + }) +} diff --git a/crypto/kzg4844/kzg4844_test.go b/crypto/kzg4844/kzg4844_test.go new file mode 100644 index 0000000..d564cfd --- /dev/null +++ b/crypto/kzg4844/kzg4844_test.go @@ -0,0 +1,93 @@ +package kzg4844 + +import ( + "crypto/sha256" + "encoding/hex" + "math/big" + "testing" + + "github.com/defiweb/go-eth/hexutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func getRandBlob(seed int64) *Blob { + blob := Blob{} + for i := 0; i < BlobLength; i += ScalarSize { + h := sha256.Sum256([]byte{byte(seed + int64(i))}) + p := new(big.Int).SetBytes(h[:]) + p = p.Mod(p, BLSModulus) + p.FillBytes(blob[i : i+ScalarSize]) + } + return &blob +} + +func getPoint(blob *Blob) (p Point) { + h := sha256.Sum256(blob[:]) + i := new(big.Int).SetBytes(h[:]) + i = i.Mod(i, BLSModulus) + i.FillBytes(p[:]) + return p +} + +func TestVerifyProof(t *testing.T) { + blob := getRandBlob(0) + point := getPoint(blob) + + // Get commitment for the blob. + commitment, err := BlobToCommitment(blob) + require.NoError(t, err) + + // Compute proof and claim. + proof, claim, err := ComputeProof(blob, point) + require.NoError(t, err) + + // Verify the proof. + err = VerifyProof(commitment, point, claim, proof) + assert.NoError(t, err) +} + +func TestBlobVerifyProof(t *testing.T) { + // Create a test blob + blob := getRandBlob(0) + + // Get commitment for the blob. + commitment, err := BlobToCommitment(blob) + require.NoError(t, err) + + // Compute proof. + proof, err := ComputeBlobProof(blob, commitment) + require.NoError(t, err) + + // Verify the proof. + err = VerifyBlobProof(blob, commitment, proof) + assert.NoError(t, err) +} + +func TestComputeBlobHashV1(t *testing.T) { + tc := []struct { + name string + commitment string + wantHash string + }{ + { + name: "zero commitment", + commitment: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + wantHash: "01b0761f87b081d5cf10757ccc89f12be355c70e2e29df288b65b30710dcbcd1", + }, + { + name: "test commitment", + commitment: "123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0", + wantHash: "012a8194ef18215aa6278923a1b143e6cf3a28087a3d26bcf6f887befc62cb47", + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + var commitment Commitment + copy(commitment[:], hexutil.MustHexToBytes(tt.commitment)) + + hash := ComputeBlobHashV1(commitment) + assert.Equal(t, tt.wantHash, hex.EncodeToString(hash[:])) + }) + } +} diff --git a/crypto/transaction.go b/crypto/transaction.go deleted file mode 100644 index 4cd9a4d..0000000 --- a/crypto/transaction.go +++ /dev/null @@ -1,109 +0,0 @@ -package crypto - -import ( - "fmt" - "math/big" - - "github.com/defiweb/go-rlp" - - "github.com/defiweb/go-eth/types" -) - -func signingHash(t *types.Transaction) (types.Hash, error) { - var ( - chainID = uint64(1) - nonce = uint64(0) - gasPrice = big.NewInt(0) - gasLimit = uint64(0) - maxPriorityFeePerGas = big.NewInt(0) - maxFeePerGas = big.NewInt(0) - to = ([]byte)(nil) - value = big.NewInt(0) - accessList = (types.AccessList)(nil) - ) - if t.ChainID != nil { - chainID = *t.ChainID - } - if t.Nonce != nil { - nonce = *t.Nonce - } - if t.GasPrice != nil { - gasPrice = t.GasPrice - } - if t.GasLimit != nil { - gasLimit = *t.GasLimit - } - if t.MaxPriorityFeePerGas != nil { - maxPriorityFeePerGas = t.MaxPriorityFeePerGas - } - if t.MaxFeePerGas != nil { - maxFeePerGas = t.MaxFeePerGas - } - if t.To != nil { - to = t.To[:] - } - if t.Value != nil { - value = t.Value - } - if t.AccessList != nil { - accessList = t.AccessList - } - switch t.Type { - case types.LegacyTxType: - list := rlp.NewList( - rlp.NewUint(nonce), - rlp.NewBigInt(gasPrice), - rlp.NewUint(gasLimit), - rlp.NewBytes(to), - rlp.NewBigInt(value), - rlp.NewBytes(t.Input), - ) - if t.ChainID != nil && *t.ChainID != 0 { - list.Append( - rlp.NewUint(chainID), - rlp.NewUint(0), - rlp.NewUint(0), - ) - } - bin, err := list.EncodeRLP() - if err != nil { - return types.Hash{}, err - } - return Keccak256(bin), nil - case types.AccessListTxType: - bin, err := rlp.NewList( - rlp.NewUint(chainID), - rlp.NewUint(nonce), - rlp.NewBigInt(gasPrice), - rlp.NewUint(gasLimit), - rlp.NewBytes(to), - rlp.NewBigInt(value), - rlp.NewBytes(t.Input), - &t.AccessList, - ).EncodeRLP() - if err != nil { - return types.Hash{}, err - } - bin = append([]byte{byte(t.Type)}, bin...) - return Keccak256(bin), nil - case types.DynamicFeeTxType: - bin, err := rlp.NewList( - rlp.NewUint(chainID), - rlp.NewUint(nonce), - rlp.NewBigInt(maxPriorityFeePerGas), - rlp.NewBigInt(maxFeePerGas), - rlp.NewUint(gasLimit), - rlp.NewBytes(to), - rlp.NewBigInt(value), - rlp.NewBytes(t.Input), - &accessList, - ).EncodeRLP() - if err != nil { - return types.Hash{}, err - } - bin = append([]byte{byte(t.Type)}, bin...) - return Keccak256(bin), nil - default: - return types.Hash{}, fmt.Errorf("invalid transaction type: %d", t.Type) - } -} diff --git a/crypto/transaction_test.go b/crypto/transaction_test.go deleted file mode 100644 index 5fe8017..0000000 --- a/crypto/transaction_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package crypto - -import ( - "fmt" - "math/big" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/defiweb/go-eth/types" -) - -func Test_singingHash(t1 *testing.T) { - tests := []struct { - tx *types.Transaction - want types.Hash - }{ - // Empty transaction: - { - tx: &types.Transaction{}, - want: types.MustHashFromHex("5460be86ce1e4ca0564b5761c6e7070d9f054b671f5404268335000806423d75", types.PadNone), - }, - // Legacy transaction: - { - tx: (&types.Transaction{}). - SetType(types.LegacyTxType). - SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetGasPrice(big.NewInt(1000000000)). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1), - want: types.MustHashFromHex("1efbe489013ac8c0dad2202f68ac12657471df8d80f70e0683ec07b0564a32ca", types.PadNone), - }, - // Access list transaction: - { - tx: (&types.Transaction{}). - SetType(types.AccessListTxType). - SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetGasPrice(big.NewInt(1000000000)). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1). - SetAccessList(types.AccessList{ - types.AccessTuple{ - Address: types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), - StorageKeys: []types.Hash{ - types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), - types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), - }, - }, - }), - want: types.MustHashFromHex("71cba0039a020b7a524d7746b79bf6d1f8a521eb1a76715d00116ef1c0f56107", types.PadNone), - }, - // Dynamic fee transaction with access list: - { - tx: (&types.Transaction{}). - SetType(types.DynamicFeeTxType). - SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1). - SetMaxPriorityFeePerGas(big.NewInt(1000000000)). - SetMaxFeePerGas(big.NewInt(2000000000)). - SetAccessList(types.AccessList{ - types.AccessTuple{ - Address: types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), - StorageKeys: []types.Hash{ - types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), - types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), - }, - }, - }), - want: types.MustHashFromHex("a66ab756479bfd56f29658a8a199319094e84711e8a2de073ec136ef5179c4c9", types.PadNone), - }, - // Dynamic fee transaction with no access list: - { - tx: (&types.Transaction{}). - SetType(types.DynamicFeeTxType). - SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1). - SetMaxPriorityFeePerGas(big.NewInt(1000000000)). - SetMaxFeePerGas(big.NewInt(2000000000)), - want: types.MustHashFromHex("c3266152306909bfe339f90fad4f73f958066860300b5a22b98ee6a1d629706c", types.PadNone), - }, - // Example from EIP-155: - { - tx: (&types.Transaction{}). - SetType(types.LegacyTxType). - SetChainID(1). - SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(types.SignatureFromVRS( - func() *big.Int { - v, _ := new(big.Int).SetString("37", 10) - return v - }(), - func() *big.Int { - v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) - return v - }(), - func() *big.Int { - v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) - return v - }(), - )), - want: types.MustHashFromHex("daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53", types.PadNone), - }, - } - for n, tt := range tests { - t1.Run(fmt.Sprintf("case-%d", n+1), func(t1 *testing.T) { - sh, err := signingHash(tt.tx) - require.NoError(t1, err) - require.Equal(t1, tt.want, sh) - }) - } -} diff --git a/crypto/txsign/txsign.go b/crypto/txsign/txsign.go new file mode 100644 index 0000000..dce0699 --- /dev/null +++ b/crypto/txsign/txsign.go @@ -0,0 +1,77 @@ +// Package txsign provides transaction signing and address recovery +// functionality for Ethereum transactions. +package txsign + +import ( + "fmt" + "math/big" + + "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/ecdsa" + "github.com/defiweb/go-eth/types" +) + +// Sign signs the given transaction with the given private key. +func Sign(key *ecdsa.PrivateKey, tx types.Transaction) error { + if key == nil { + return fmt.Errorf("missing private key") + } + txd := tx.GetTransactionData() + hash, err := tx.CalculateSigningHash() + if err != nil { + return err + } + sig, err := crypto.ECSignHash(key, ecdsa.Hash(hash)) + if err != nil { + return err + } + sv, sr, ss := sig.V, sig.R, sig.S + if tx.Type() == types.LegacyTxType { + if txd.ChainID != nil { + sv = new(big.Int).Add(sv, new(big.Int).SetUint64(*txd.ChainID*2)) + sv = new(big.Int).Add(sv, big.NewInt(35)) + } else { + sv = new(big.Int).Add(sv, big.NewInt(27)) + } + } + txd.SetSignature(types.SignatureFromVRS(sv, sr, ss)) + if cd, ok := tx.(types.HasCallData); ok { + cd.GetCallData().SetFrom(types.Address(crypto.ECPublicKeyToAddress(crypto.ECPrivateKeyToPublicKey(key)))) + } + return nil +} + +// Recover recovers the Ethereum address from the given transaction's +// signature. +func Recover(tx types.Transaction) (*types.Address, error) { + txd := tx.GetTransactionData() + if txd.Signature == nil { + return nil, fmt.Errorf("signature is missing") + } + sig := *txd.Signature + if tx.Type() == types.LegacyTxType { + if sig.V.Cmp(big.NewInt(35)) >= 0 { + x := new(big.Int).Sub(sig.V, big.NewInt(35)) + + // Derive the chain ID from the signature. + chainID := new(big.Int).Div(x, big.NewInt(2)) + if txd.ChainID != nil && *txd.ChainID != chainID.Uint64() { + return nil, fmt.Errorf("invalid chain ID: %d", chainID) + } + + // Derive the recovery byte from the signature. + sig.V = new(big.Int).Add(new(big.Int).Mod(x, big.NewInt(2)), big.NewInt(27)) + } else { + sig.V = new(big.Int).Sub(sig.V, big.NewInt(27)) + } + } + hash, err := tx.CalculateSigningHash() + if err != nil { + return nil, err + } + addr, err := crypto.ECRecoverHash(ecdsa.Hash(hash), ecdsa.Signature(sig)) + if err != nil { + return nil, err + } + return (*types.Address)(addr), nil +} diff --git a/crypto/txsign/txsing_test.go b/crypto/txsign/txsing_test.go new file mode 100644 index 0000000..5b3ceec --- /dev/null +++ b/crypto/txsign/txsing_test.go @@ -0,0 +1,96 @@ +package txsign + +import ( + "bytes" + "math/big" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/defiweb/go-eth/crypto/ecdsa" + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSign(t *testing.T) { + t.Run("legacy", func(t *testing.T) { + key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) + tx := types.NewTransactionLegacy() + tx.SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")) + tx.SetGasLimit(21000) + tx.SetGasPrice(big.NewInt(20000000000)) + tx.SetNonce(9) + tx.SetValue(big.NewInt(1000000000000000000)) + + err := Sign(&ecdsa.PrivateKey{D: key.ToECDSA().D}, tx) + require.NoError(t, err) + txData := tx.GetTransactionData() + require.NotNil(t, txData.Signature) + assert.Equal(t, "1b", txData.Signature.V.Text(16)) + assert.Equal(t, "2bfad43ba1b40e7f3ffb6342b1a6eecc700dd344fb0aba543aed5c10fd1a9470", txData.Signature.R.Text(16)) + assert.Equal(t, "615bff48c483d368ed4f6e327a6ddd8831e544d0ca08f1345433e4ed204f8537", txData.Signature.S.Text(16)) + }) + t.Run("dynamic-fee", func(t *testing.T) { + key, _ := btcec.PrivKeyFromBytes(bytes.Repeat([]byte{0x01}, 32)) + tx := types.NewTransactionDynamicFee() + tx.SetChainID(1) + tx.SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")) + tx.SetGasLimit(21000) + tx.SetMaxFeePerGas(big.NewInt(20000000000)) + tx.SetMaxPriorityFeePerGas(big.NewInt(20000000000)) + tx.SetNonce(9) + tx.SetValue(big.NewInt(1000000000000000000)) + + err := Sign(&ecdsa.PrivateKey{D: key.ToECDSA().D}, tx) + require.NoError(t, err) + txData := tx.GetTransactionData() + require.NotNil(t, txData.Signature) + assert.Equal(t, "0", txData.Signature.V.Text(16)) + assert.Equal(t, "62072d055f9ceb871a47f2d81aeb5aa34df50c625da16c6d0d57d232fa3cd152", txData.Signature.R.Text(16)) + assert.Equal(t, "57fd88df7c85076f5729493be7e87f51b618a78bc89441ed741bdfdb9d1d5572", txData.Signature.S.Text(16)) + }) +} + +func TestRecover(t *testing.T) { + t.Run("legacy", func(t *testing.T) { + tx := types.NewTransactionLegacy() + tx.SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")) + tx.SetGasLimit(21000) + tx.SetGasPrice(big.NewInt(20000000000)) + tx.SetNonce(9) + tx.SetValue(big.NewInt(1000000000000000000)) + txData := tx.GetTransactionData() + txData.SetSignature(types.SignatureFromVRS( + hexutil.MustHexToBigInt("1b"), + hexutil.MustHexToBigInt("2bfad43ba1b40e7f3ffb6342b1a6eecc700dd344fb0aba543aed5c10fd1a9470"), + hexutil.MustHexToBigInt("615bff48c483d368ed4f6e327a6ddd8831e544d0ca08f1345433e4ed204f8537"), + )) + + addr, err := Recover(tx) + require.NoError(t, err) + require.NotNil(t, addr) + assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) + }) + t.Run("dynamic-fee", func(t *testing.T) { + tx := types.NewTransactionDynamicFee() + tx.SetChainID(1) + tx.SetTo(types.MustAddressFromHex("0x3535353535353535353535353535353535353535")) + tx.SetGasLimit(21000) + tx.SetMaxFeePerGas(big.NewInt(20000000000)) + tx.SetMaxPriorityFeePerGas(big.NewInt(20000000000)) + tx.SetNonce(9) + tx.SetValue(big.NewInt(1000000000000000000)) + txData := tx.GetTransactionData() + txData.SetSignature(types.SignatureFromVRS( + hexutil.MustHexToBigInt("0"), + hexutil.MustHexToBigInt("62072d055f9ceb871a47f2d81aeb5aa34df50c625da16c6d0d57d232fa3cd152"), + hexutil.MustHexToBigInt("57fd88df7c85076f5729493be7e87f51b618a78bc89441ed741bdfdb9d1d5572"), + )) + + addr, err := Recover(tx) + require.NoError(t, err) + require.NotNil(t, addr) + assert.Equal(t, "0x1a642f0e3c3af545e7acbd38b07251b3990914f1", addr.String()) + }) +} diff --git a/examples/call-abi/main.go b/examples/call-abi/main.go index 07ebfcd..31b6f0e 100644 --- a/examples/call-abi/main.go +++ b/examples/call-abi/main.go @@ -73,12 +73,12 @@ func main() { }) // Prepare a call. - call := types.NewCall(). - SetTo(types.MustAddressFromHex("0xcA11bde05977b3631167028862bE2a173976CA11")). - SetInput(calldata) + call := types.NewCallLegacy() + call.SetTo(types.MustAddressFromHex("0xcA11bde05977b3631167028862bE2a173976CA11")) + call.SetInput(calldata) // Call the contract. - b, _, err := c.Call(context.Background(), call, types.LatestBlockNumber) + b, err := c.Call(context.Background(), call, types.LatestBlockNumber) if err != nil { panic(err) } diff --git a/examples/call/main.go b/examples/call/main.go index 1acbe17..c1928b2 100644 --- a/examples/call/main.go +++ b/examples/call/main.go @@ -31,12 +31,12 @@ func main() { calldata := balanceOf.MustEncodeArgs("0xd8da6bf26964af9d7eed9e03e53415d37aa96045") // Prepare a call. - call := types.NewCall(). - SetTo(types.MustAddressFromHex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")). - SetInput(calldata) + call := types.NewCallLegacy() + call.SetTo(types.MustAddressFromHex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")) + call.SetInput(calldata) // Call balanceOf. - b, _, err := c.Call(context.Background(), call, types.LatestBlockNumber) + b, err := c.Call(context.Background(), call, types.LatestBlockNumber) if err != nil { panic(err) } diff --git a/examples/contract-json-abi/main.go b/examples/contract-json-abi/main.go index 2c9ed85..685df92 100644 --- a/examples/contract-json-abi/main.go +++ b/examples/contract-json-abi/main.go @@ -3,12 +3,13 @@ package main import ( "fmt" "math/big" + "os" "github.com/defiweb/go-eth/abi" ) func main() { - erc20, err := abi.LoadJSON("erc20.json") + erc20, err := abi.LoadJSON(abiPath()) if err != nil { panic(err) } @@ -24,3 +25,10 @@ func main() { fmt.Printf("Transfer calldata: 0x%x\n", calldata) } + +func abiPath() string { + if _, err := os.Stat("./erc20.json"); err == nil { + return "./key.json" + } + return "./examples/contract-json-abi/erc20.json" +} diff --git a/examples/events/main.go b/examples/events/main.go index 77eb0bc..0cef80a 100644 --- a/examples/events/main.go +++ b/examples/events/main.go @@ -27,11 +27,11 @@ func main() { transfer := abi.MustParseEvent("Transfer(address indexed src, address indexed dst, uint256 wad)") // Create filter query. - query := types.NewFilterLogsQuery(). - SetAddresses(types.MustAddressFromHex("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")). - SetFromBlock(types.BlockNumberFromUint64Ptr(16492400)). - SetToBlock(types.BlockNumberFromUint64Ptr(16492400)). - SetTopics([]types.Hash{transfer.Topic0()}) + query := types.NewFilterLogsQuery() + query.SetAddresses(types.MustAddressFromHex("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")) + query.SetFromBlock(types.BlockNumberFromUint64Ptr(16492400)) + query.SetToBlock(types.BlockNumberFromUint64Ptr(16492400)) + query.SetTopics([]types.Hash{transfer.Topic0()}) // Fetch logs for WETH transfer events. logs, err := c.GetLogs(context.Background(), query) diff --git a/examples/send-tx/main.go b/examples/send-tx/main.go index 3b6545f..99dde7d 100644 --- a/examples/send-tx/main.go +++ b/examples/send-tx/main.go @@ -4,18 +4,18 @@ import ( "context" "fmt" "math/big" + "os" "github.com/defiweb/go-eth/abi" "github.com/defiweb/go-eth/rpc" "github.com/defiweb/go-eth/rpc/transport" - "github.com/defiweb/go-eth/txmodifier" "github.com/defiweb/go-eth/types" "github.com/defiweb/go-eth/wallet" ) func main() { // Load the private key. - key, err := wallet.NewKeyFromJSON("./key.json", "test123") + k, err := wallet.NewKeyFromJSON(keyPath(), "test123") if err != nil { panic(err) } @@ -32,42 +32,33 @@ func main() { rpc.WithTransport(t), // Specify a key for signing transactions. If provided, the client - // uses it with SignTransaction, SendTransaction, and Sign methods - // instead of relying on the node for signing. - rpc.WithKeys(key), - - // Specify a default address for SendTransaction when the transaction - // does not have a 'From' field set. - rpc.WithDefaultAddress(key.Address()), - - // TX modifiers enable modifications to the transaction before signing - // and sending to the node. While not mandatory, without them, transaction - // parameters like gas limit, gas price, and nonce must be set manually. - rpc.WithTXModifiers( - // GasLimitEstimator automatically estimates the gas limit for the - // transaction. - txmodifier.NewGasLimitEstimator(txmodifier.GasLimitEstimatorOptions{ - Multiplier: 1.25, - }), - - // GasFeeEstimator automatically estimates the gas price for the - // transaction based on the current market conditions. - txmodifier.NewEIP1559GasFeeEstimator(txmodifier.EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.25, - PriorityFeePerGasMultiplier: 1.25, - }), - - // NonceProvider automatically sets the nonce for the transaction. - txmodifier.NewNonceProvider(txmodifier.NonceProviderOptions{ - UsePendingBlock: false, - }), - - // ChainIDProvider automatically sets the chain ID for the transaction. - txmodifier.NewChainIDProvider(txmodifier.ChainIDProviderOptions{ - Replace: false, - Cache: true, - }), - ), + // will sign transactions before sending them to the node. + rpc.WithKeys(k), + + // Specify the default "from" address for transactions. + rpc.WithDefaultAddress(rpc.AddressOptions{ + Address: k.Address(), + }), + + // Estimate gas limit for transactions if not provided explicitly. + rpc.WithGasLimit(rpc.GasLimitOptions{ + Multiplier: 1.25, + }), + + // Estimate gas price for transactions if not provided explicitly. + rpc.WithDynamicGasFee(rpc.DynamicGasFeeOptions{ + GasPriceMultiplier: 1.25, + PriorityFeePerGasMultiplier: 1.25, + }), + + // Automatically set the chain ID for transactions. + rpc.WithChainID(rpc.ChainIDOptions{}), + + // Automatically set the nonce for transactions. + rpc.WithNonce(rpc.NonceOptions{}), + + // Simulate transactions before sending them to the node. + rpc.WithSimulate(), ) if err != nil { panic(err) @@ -80,11 +71,11 @@ func main() { calldata := transfer.MustEncodeArgs("0xd8da6bf26964af9d7eed9e03e53415d37aa96045", new(big.Int).Mul(big.NewInt(100), big.NewInt(1e6))) // Prepare a transaction. - tx := types.NewTransaction(). - SetTo(types.MustAddressFromHex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")). - SetInput(calldata) + tx := types.NewTransactionLegacy() + tx.SetTo(types.MustAddressFromHex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")) + tx.SetInput(calldata) - txHash, _, err := c.SendTransaction(context.Background(), tx) + txHash, err := c.SendTransaction(context.Background(), tx) if err != nil { panic(err) } @@ -92,3 +83,10 @@ func main() { // Print the transaction hash. fmt.Printf("Transaction hash: %s\n", txHash.String()) } + +func keyPath() string { + if _, err := os.Stat("./key.json"); err == nil { + return "./key.json" + } + return "./examples/send-tx/key.json" +} diff --git a/go.mod b/go.mod index 637cc59..893407c 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ require ( github.com/btcsuite/btcd v0.24.0 github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/crate-crypto/go-kzg-4844 v1.1.0 github.com/defiweb/go-anymapper v0.3.0 - github.com/defiweb/go-rlp v0.3.0 + github.com/defiweb/go-rlp v0.4.0 github.com/defiweb/go-sigparser v0.6.0 github.com/stretchr/testify v1.8.4 github.com/tyler-smith/go-bip39 v1.1.0 @@ -18,14 +19,16 @@ require ( ) require ( + github.com/bits-and-blooms/bitset v1.7.0 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.13.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/stretchr/objx v0.5.1 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.16.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index 53b1036..c33e353 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= +github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -25,7 +27,12 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.13.0 h1:VPULb/v6bbYELAPTDFINEVaMTTybV5GLxDdcjnS+4oc= +github.com/consensys/gnark-crypto v0.13.0/go.mod h1:wKqwsieaKPThcFkHe0d0zMsbHEUWFmZcG7KBCse210o= +github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= +github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -40,6 +47,8 @@ github.com/defiweb/go-anymapper v0.3.0 h1:sWbTvhpdBaCHQGn+kuKYDnb+mPmeDNzzEXnC+C github.com/defiweb/go-anymapper v0.3.0/go.mod h1:EeQDyOsFd63Pt2uu9Yb8NFrChuZ9JBChjGKbDhRPHAQ= github.com/defiweb/go-rlp v0.3.0 h1:0q+EuR5SdSDu7XLx5Cu68EwVSaNA+CkRCFcE+17HNxA= github.com/defiweb/go-rlp v0.3.0/go.mod h1:nLGzk10jAgynPvN2hL+tLnnyZ5Fcshv0wmpWDRtV0PA= +github.com/defiweb/go-rlp v0.4.0 h1:IheGseUX+kDlykSiADAWYo4J5kSaJPlBraLOfzAsSlI= +github.com/defiweb/go-rlp v0.4.0/go.mod h1:nLGzk10jAgynPvN2hL+tLnnyZ5Fcshv0wmpWDRtV0PA= github.com/defiweb/go-sigparser v0.6.0 h1:HSNAZSUl8xyV+nKfWNKYVAPWLwTuASas6ohtarBbOT4= github.com/defiweb/go-sigparser v0.6.0/go.mod h1:R1wkfsnASR2M38ZupKHoqqIfv+8HgRbZaFQI9Inr4k8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -55,19 +64,19 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -77,21 +86,11 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -108,6 +107,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -133,14 +134,16 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/hexutil/hexutil.go b/hexutil/hexutil.go index 6475281..0ed833b 100644 --- a/hexutil/hexutil.go +++ b/hexutil/hexutil.go @@ -1,3 +1,5 @@ +// Package hexutil provides utilities for working with hexadecimal-encoded +// data. package hexutil import ( @@ -6,9 +8,9 @@ import ( "math/big" ) -// BigIntToHex returns the hex representation of the given big integer. -// The hex string is prefixed with "0x". Negative numbers are prefixed with -// "-0x". +// BigIntToHex returns the hex representation of the given [big.Int]. +// The hex string is prefixed with "0x". +// Negative numbers are prefixed with "-0x". func BigIntToHex(x *big.Int) string { if x == nil { return "0x0" @@ -24,7 +26,7 @@ func BigIntToHex(x *big.Int) string { } } -// HexToBigInt returns the big integer representation of the given hex string. +// HexToBigInt returns the [big.Int] representation of the given hex string. // The hex string may be prefixed with "0x". func HexToBigInt(h string) (*big.Int, error) { isNeg := len(h) > 1 && h[0] == '-' @@ -44,6 +46,7 @@ func HexToBigInt(h string) (*big.Int, error) { return x, nil } +// MustHexToBigInt works like [HexToBigInt] but panics on error. func MustHexToBigInt(h string) *big.Int { x, err := HexToBigInt(h) if err != nil { @@ -52,18 +55,18 @@ func MustHexToBigInt(h string) *big.Int { return x } -// BytesToHex returns the hex representation of the given bytes. The hex string -// is always even-length and prefixed with "0x". +// BytesToHex returns the hex representation of the given bytes. +// The hex string is always even-length and prefixed with "0x". func BytesToHex(b []byte) string { r := make([]byte, len(b)*2+2) - copy(r, `0x`) + copy(r, "0x") hex.Encode(r[2:], b) return string(r) } // HexToBytes returns the bytes representation of the given hex string. -// The number of hex digits must be even. The hex string may be prefixed with -// "0x". +// The number of hex digits must be even. +// The hex string may be prefixed with "0x". func HexToBytes(h string) ([]byte, error) { if len(h) == 0 { return []byte{}, nil @@ -83,6 +86,7 @@ func HexToBytes(h string) ([]byte, error) { return hex.DecodeString(h) } +// MustHexToBytes works like [HexToBytes] but panics on error. func MustHexToBytes(h string) []byte { b, err := HexToBytes(h) if err != nil { @@ -91,7 +95,7 @@ func MustHexToBytes(h string) []byte { return b } -// Has0xPrefix returns true if the given byte slice starts with "0x". +// Has0xPrefix returns true if the given string starts with "0x" or "0X". func Has0xPrefix(h string) bool { return len(h) >= 2 && h[0] == '0' && (h[1] == 'x' || h[1] == 'X') } diff --git a/hexutil/hexutil_test.go b/hexutil/hexutil_test.go index bbd4af1..a2ddbf8 100644 --- a/hexutil/hexutil_test.go +++ b/hexutil/hexutil_test.go @@ -9,7 +9,7 @@ import ( ) func TestBigIntToHex(t *testing.T) { - tests := []struct { + tc := []struct { name string input *big.Int expected string @@ -19,8 +19,7 @@ func TestBigIntToHex(t *testing.T) { {"positive value", big.NewInt(26), "0x1a"}, {"negative value", big.NewInt(-26), "-0x1a"}, } - - for _, tt := range tests { + for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, BigIntToHex(tt.input)) }) @@ -28,7 +27,7 @@ func TestBigIntToHex(t *testing.T) { } func TestHexToBigInt(t *testing.T) { - tests := []struct { + tc := []struct { name string input string expected *big.Int @@ -45,8 +44,7 @@ func TestHexToBigInt(t *testing.T) { {"empty string", "", nil, fmt.Errorf("invalid hex string")}, {"invalid hex", "0x1g", nil, fmt.Errorf("invalid hex string")}, } - - for _, tt := range tests { + for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { result, err := HexToBigInt(tt.input) assert.Equal(t, tt.err, err) @@ -56,7 +54,7 @@ func TestHexToBigInt(t *testing.T) { } func TestBytesToHex(t *testing.T) { - tests := []struct { + tc := []struct { name string input []byte expected string @@ -65,8 +63,7 @@ func TestBytesToHex(t *testing.T) { {"non-empty bytes", []byte("abc"), "0x616263"}, {"bytes with zeros", []byte{0, 1, 2}, "0x000102"}, } - - for _, tt := range tests { + for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, BytesToHex(tt.input)) }) @@ -74,7 +71,7 @@ func TestBytesToHex(t *testing.T) { } func TestHexToBytes(t *testing.T) { - tests := []struct { + tc := []struct { name string input string expected []byte @@ -87,8 +84,7 @@ func TestHexToBytes(t *testing.T) { {"single zero", "0", []byte{0}, nil}, {"invalid hex", "0x1", nil, fmt.Errorf("invalid hex string, length must be even")}, } - - for _, tt := range tests { + for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { result, err := HexToBytes(tt.input) assert.Equal(t, tt.err, err) diff --git a/rpc/base.go b/rpc/base.go deleted file mode 100644 index 1fd6dd8..0000000 --- a/rpc/base.go +++ /dev/null @@ -1,484 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/big" - - "github.com/defiweb/go-eth/rpc/transport" - "github.com/defiweb/go-eth/types" -) - -// baseClient is a base implementation of the RPC interface. It implements -// RPC methods supported by Ethereum nodes. -type baseClient struct { - transport transport.Transport -} - -// ClientVersion implements the RPC interface. -func (c *baseClient) ClientVersion(ctx context.Context) (string, error) { - var res string - if err := c.transport.Call(ctx, &res, "web3_clientVersion"); err != nil { - return "", err - } - return res, nil -} - -// Listening implements the RPC interface. -func (c *baseClient) Listening(ctx context.Context) (bool, error) { - var res bool - if err := c.transport.Call(ctx, &res, "net_listening"); err != nil { - return false, err - } - return res, nil -} - -// PeerCount implements the RPC interface. -func (c *baseClient) PeerCount(ctx context.Context) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "net_peerCount"); err != nil { - return 0, err - } - return res.Big().Uint64(), nil -} - -// ProtocolVersion implements the RPC interface. -func (c *baseClient) ProtocolVersion(ctx context.Context) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_protocolVersion"); err != nil { - return 0, err - } - return res.Big().Uint64(), nil -} - -// Syncing implements the RPC interface. -func (c *baseClient) Syncing(ctx context.Context) (*types.SyncStatus, error) { - var res types.SyncStatus - if err := c.transport.Call(ctx, &res, "eth_syncing"); err != nil { - return nil, err - } - return &res, nil -} - -// NetworkID implements the RPC interface. -func (c *baseClient) NetworkID(ctx context.Context) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "net_version"); err != nil { - return 0, err - } - return res.Big().Uint64(), nil -} - -// ChainID implements the RPC interface. -func (c *baseClient) ChainID(ctx context.Context) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_chainId"); err != nil { - return 0, err - } - if !res.Big().IsUint64() { - return 0, fmt.Errorf("chain id is too big") - } - return res.Big().Uint64(), nil -} - -// GasPrice implements the RPC interface. -func (c *baseClient) GasPrice(ctx context.Context) (*big.Int, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_gasPrice"); err != nil { - return nil, err - } - return res.Big(), nil -} - -// Accounts implements the RPC interface. -func (c *baseClient) Accounts(ctx context.Context) ([]types.Address, error) { - var res []types.Address - if err := c.transport.Call(ctx, &res, "eth_accounts"); err != nil { - return nil, err - } - return res, nil -} - -// BlockNumber implements the RPC interface. -func (c *baseClient) BlockNumber(ctx context.Context) (*big.Int, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_blockNumber"); err != nil { - return nil, err - } - return res.Big(), nil -} - -// GetBalance implements the RPC interface. -func (c *baseClient) GetBalance(ctx context.Context, address types.Address, block types.BlockNumber) (*big.Int, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_getBalance", address, block); err != nil { - return nil, err - } - return res.Big(), nil -} - -// GetStorageAt implements the RPC interface. -func (c *baseClient) GetStorageAt(ctx context.Context, account types.Address, key types.Hash, block types.BlockNumber) (*types.Hash, error) { - var res types.Hash - if err := c.transport.Call(ctx, &res, "eth_getStorageAt", account, key, block); err != nil { - return nil, err - } - return &res, nil -} - -// GetTransactionCount implements the RPC interface. -func (c *baseClient) GetTransactionCount(ctx context.Context, account types.Address, block types.BlockNumber) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_getTransactionCount", account, block); err != nil { - return 0, err - } - if !res.Big().IsUint64() { - return 0, errors.New("transaction count is too big") - } - return res.Big().Uint64(), nil -} - -// GetBlockTransactionCountByHash implements the RPC interface. -func (c *baseClient) GetBlockTransactionCountByHash(ctx context.Context, hash types.Hash) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_getBlockTransactionCountByHash", hash); err != nil { - return 0, err - } - if !res.Big().IsUint64() { - return 0, errors.New("transaction count is too big") - } - return res.Big().Uint64(), nil -} - -// GetBlockTransactionCountByNumber implements the RPC interface. -func (c *baseClient) GetBlockTransactionCountByNumber(ctx context.Context, number types.BlockNumber) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_getBlockTransactionCountByNumber", number); err != nil { - return 0, err - } - if !res.Big().IsUint64() { - return 0, errors.New("transaction count is too big") - } - return res.Big().Uint64(), nil -} - -// GetUncleCountByBlockHash implements the RPC interface. -func (c *baseClient) GetUncleCountByBlockHash(ctx context.Context, hash types.Hash) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_getUncleCountByBlockHash", hash); err != nil { - return 0, err - } - if !res.Big().IsUint64() { - return 0, errors.New("uncle count is too big") - } - return res.Big().Uint64(), nil -} - -// GetUncleCountByBlockNumber implements the RPC interface. -func (c *baseClient) GetUncleCountByBlockNumber(ctx context.Context, number types.BlockNumber) (uint64, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_getUncleCountByBlockNumber", number); err != nil { - return 0, err - } - if !res.Big().IsUint64() { - return 0, errors.New("uncle count is too big") - } - return res.Big().Uint64(), nil -} - -// GetCode implements the RPC interface. -func (c *baseClient) GetCode(ctx context.Context, account types.Address, block types.BlockNumber) ([]byte, error) { - var res types.Bytes - if err := c.transport.Call(ctx, &res, "eth_getCode", account, block); err != nil { - return nil, err - } - return res.Bytes(), nil -} - -// Sign implements the RPC interface. -func (c *baseClient) Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) { - var res types.Signature - if err := c.transport.Call(ctx, &res, "eth_sign", account, types.Bytes(data)); err != nil { - return nil, err - } - return &res, nil -} - -// SignTransaction implements the RPC interface. -func (c *baseClient) SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, *types.Transaction, error) { - if tx == nil { - return nil, nil, errors.New("rpc client: transaction is nil") - } - var res signTransactionResult - if err := c.transport.Call(ctx, &res, "eth_signTransaction", tx); err != nil { - return nil, nil, err - } - return res.Raw, res.Tx, nil -} - -// SendTransaction implements the RPC interface. -func (c *baseClient) SendTransaction(ctx context.Context, tx *types.Transaction) (*types.Hash, *types.Transaction, error) { - if tx == nil { - return nil, nil, errors.New("rpc client: transaction is nil") - } - var res types.Hash - if err := c.transport.Call(ctx, &res, "eth_sendTransaction", tx); err != nil { - return nil, nil, err - } - return &res, tx, nil -} - -// SendRawTransaction implements the RPC interface. -func (c *baseClient) SendRawTransaction(ctx context.Context, data []byte) (*types.Hash, error) { - var res types.Hash - if err := c.transport.Call(ctx, &res, "eth_sendRawTransaction", types.Bytes(data)); err != nil { - return nil, err - } - return &res, nil -} - -// Call implements the RPC interface. -func (c *baseClient) Call(ctx context.Context, call *types.Call, block types.BlockNumber) ([]byte, *types.Call, error) { - if call == nil { - return nil, nil, errors.New("rpc client: call is nil") - } - var res types.Bytes - if err := c.transport.Call(ctx, &res, "eth_call", call, block); err != nil { - return nil, nil, err - } - return res, call, nil -} - -// EstimateGas implements the RPC interface. -func (c *baseClient) EstimateGas(ctx context.Context, call *types.Call, block types.BlockNumber) (uint64, *types.Call, error) { - if call == nil { - return 0, nil, errors.New("rpc client: call is nil") - } - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_estimateGas", call, block); err != nil { - return 0, nil, err - } - if !res.Big().IsUint64() { - return 0, nil, errors.New("gas estimate is too big") - } - return res.Big().Uint64(), call, nil -} - -// BlockByHash implements the RPC interface. -func (c *baseClient) BlockByHash(ctx context.Context, hash types.Hash, full bool) (*types.Block, error) { - var res types.Block - if err := c.transport.Call(ctx, &res, "eth_getBlockByHash", hash, full); err != nil { - return nil, err - } - return &res, nil -} - -// BlockByNumber implements the RPC interface. -func (c *baseClient) BlockByNumber(ctx context.Context, number types.BlockNumber, full bool) (*types.Block, error) { - var res types.Block - if err := c.transport.Call(ctx, &res, "eth_getBlockByNumber", number, full); err != nil { - return nil, err - } - return &res, nil -} - -// GetTransactionByHash implements the RPC interface. -func (c *baseClient) GetTransactionByHash(ctx context.Context, hash types.Hash) (*types.OnChainTransaction, error) { - var res types.OnChainTransaction - if err := c.transport.Call(ctx, &res, "eth_getTransactionByHash", hash); err != nil { - return nil, err - } - return &res, nil -} - -// GetTransactionByBlockHashAndIndex implements the RPC interface. -func (c *baseClient) GetTransactionByBlockHashAndIndex(ctx context.Context, hash types.Hash, index uint64) (*types.OnChainTransaction, error) { - var res types.OnChainTransaction - if err := c.transport.Call(ctx, &res, "eth_getTransactionByBlockHashAndIndex", hash, types.NumberFromUint64(index)); err != nil { - return nil, err - } - return &res, nil -} - -// GetTransactionByBlockNumberAndIndex implements the RPC interface. -func (c *baseClient) GetTransactionByBlockNumberAndIndex(ctx context.Context, number types.BlockNumber, index uint64) (*types.OnChainTransaction, error) { - var res types.OnChainTransaction - if err := c.transport.Call(ctx, &res, "eth_getTransactionByBlockNumberAndIndex", number, types.NumberFromUint64(index)); err != nil { - return nil, err - } - return &res, nil -} - -// GetTransactionReceipt implements the RPC interface. -func (c *baseClient) GetTransactionReceipt(ctx context.Context, hash types.Hash) (*types.TransactionReceipt, error) { - var res types.TransactionReceipt - if err := c.transport.Call(ctx, &res, "eth_getTransactionReceipt", hash); err != nil { - return nil, err - } - return &res, nil -} - -// GetBlockReceipts implements the RPC interface. -func (c *baseClient) GetBlockReceipts(ctx context.Context, block types.BlockNumber) ([]*types.TransactionReceipt, error) { - var res []*types.TransactionReceipt - if err := c.transport.Call(ctx, &res, "eth_getBlockReceipts", block); err != nil { - return nil, err - } - return res, nil -} - -// GetUncleByBlockHashAndIndex implements the RPC interface. -func (c *baseClient) GetUncleByBlockHashAndIndex(ctx context.Context, hash types.Hash, index uint64) (*types.Block, error) { - var res types.Block - if err := c.transport.Call(ctx, &res, "eth_getUncleByBlockHashAndIndex", hash, types.NumberFromUint64(index)); err != nil { - return nil, err - } - return &res, nil -} - -// GetUncleByBlockNumberAndIndex implements the RPC interface. -func (c *baseClient) GetUncleByBlockNumberAndIndex(ctx context.Context, number types.BlockNumber, index uint64) (*types.Block, error) { - var res types.Block - if err := c.transport.Call(ctx, &res, "eth_getUncleByBlockNumberAndIndex", number, types.NumberFromUint64(index)); err != nil { - return nil, err - } - return &res, nil -} - -// NewFilter implements the RPC interface. -func (c *baseClient) NewFilter(ctx context.Context, query *types.FilterLogsQuery) (*big.Int, error) { - var res *types.Number - if err := c.transport.Call(ctx, &res, "eth_newFilter", query); err != nil { - return nil, err - } - return res.Big(), nil -} - -// NewBlockFilter implements the RPC interface. -func (c *baseClient) NewBlockFilter(ctx context.Context) (*big.Int, error) { - var res *types.Number - if err := c.transport.Call(ctx, &res, "eth_newBlockFilter"); err != nil { - return nil, err - } - return res.Big(), nil - -} - -// NewPendingTransactionFilter implements the RPC interface. -func (c *baseClient) NewPendingTransactionFilter(ctx context.Context) (*big.Int, error) { - var res *types.Number - if err := c.transport.Call(ctx, &res, "eth_newPendingTransactionFilter"); err != nil { - return nil, err - } - return res.Big(), nil -} - -// UninstallFilter implements the RPC interface. -func (c *baseClient) UninstallFilter(ctx context.Context, id *big.Int) (bool, error) { - var res bool - if err := c.transport.Call(ctx, &res, "eth_uninstallFilter", types.NumberFromBigInt(id)); err != nil { - return false, err - } - return res, nil -} - -// GetFilterChanges implements the RPC interface. -func (c *baseClient) GetFilterChanges(ctx context.Context, id *big.Int) ([]types.Log, error) { - var res []types.Log - if err := c.transport.Call(ctx, &res, "eth_getFilterChanges", types.NumberFromBigInt(id)); err != nil { - return nil, err - } - return res, nil -} - -// GetFilterLogs implements the RPC interface. -func (c *baseClient) GetFilterLogs(ctx context.Context, id *big.Int) ([]types.Log, error) { - var res []types.Log - if err := c.transport.Call(ctx, &res, "eth_getFilterLogs", types.NumberFromBigInt(id)); err != nil { - return nil, err - } - return res, nil -} - -// GetBlockFilterChanges implements the RPC interface. -func (c *baseClient) GetBlockFilterChanges(ctx context.Context, id *big.Int) ([]types.Hash, error) { - var res []types.Hash - if err := c.transport.Call(ctx, &res, "eth_getFilterChanges", types.NumberFromBigInt(id)); err != nil { - return nil, err - } - return res, nil -} - -// GetLogs implements the RPC interface. -func (c *baseClient) GetLogs(ctx context.Context, query *types.FilterLogsQuery) ([]types.Log, error) { - var res []types.Log - if err := c.transport.Call(ctx, &res, "eth_getLogs", query); err != nil { - return nil, err - } - return res, nil -} - -// MaxPriorityFeePerGas implements the RPC interface. -func (c *baseClient) MaxPriorityFeePerGas(ctx context.Context) (*big.Int, error) { - var res types.Number - if err := c.transport.Call(ctx, &res, "eth_maxPriorityFeePerGas"); err != nil { - return nil, err - } - return res.Big(), nil -} - -// SubscribeLogs implements the RPC interface. -func (c *baseClient) SubscribeLogs(ctx context.Context, query *types.FilterLogsQuery) (<-chan types.Log, error) { - return subscribe[types.Log](ctx, c.transport, "logs", query) -} - -// SubscribeNewHeads implements the RPC interface. -func (c *baseClient) SubscribeNewHeads(ctx context.Context) (<-chan types.Block, error) { - return subscribe[types.Block](ctx, c.transport, "newHeads") -} - -// SubscribeNewPendingTransactions implements the RPC interface. -func (c *baseClient) SubscribeNewPendingTransactions(ctx context.Context) (<-chan types.Hash, error) { - return subscribe[types.Hash](ctx, c.transport, "newPendingTransactions") -} - -// subscribe creates a subscription to the given method and returns a channel -// that will receive the subscription messages. The messages are unmarshalled -// to the T type. The subscription is unsubscribed and channel closed when the -// context is cancelled. -func subscribe[T any](ctx context.Context, t transport.Transport, method string, params ...any) (chan T, error) { - st, ok := t.(transport.SubscriptionTransport) - if !ok { - return nil, errors.New("transport does not support subscriptions") - } - rawCh, subID, err := st.Subscribe(ctx, method, params...) - if err != nil { - return nil, err - } - msgCh := make(chan T) - go subscriptionRoutine(ctx, st, subID, rawCh, msgCh) - return msgCh, nil -} - -//nolint:errcheck -func subscriptionRoutine[T any](ctx context.Context, t transport.SubscriptionTransport, subID string, rawCh chan json.RawMessage, msgCh chan T) { - defer close(msgCh) - defer t.Unsubscribe(ctx, subID) - for { - select { - case <-ctx.Done(): - return - case raw, ok := <-rawCh: - if !ok { - return - } - var msg T - if err := json.Unmarshal(raw, &msg); err != nil { - continue - } - msgCh <- msg - } - } -} diff --git a/rpc/base_test.go b/rpc/base_test.go deleted file mode 100644 index 130a168..0000000 --- a/rpc/base_test.go +++ /dev/null @@ -1,2087 +0,0 @@ -package rpc - -import ( - "bytes" - "context" - "encoding/json" - "io" - "math/big" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/defiweb/go-eth/hexutil" - "github.com/defiweb/go-eth/types" -) - -const mockClientVersionRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "web3_clientVersion", - "params": [] - } -` - -const mockClientVersionResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "Geth/v1.9.25-unstable-3f0b5e4e-20201014/linux-amd64/go1.15.2" - } -` - -func TestBaseClient_ClientVersion(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockClientVersionResponse)), - } - - clientVersion, err := client.ClientVersion(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockClientVersionRequest, readBody(httpMock.Request)) - assert.Equal(t, "Geth/v1.9.25-unstable-3f0b5e4e-20201014/linux-amd64/go1.15.2", clientVersion) -} - -const mockListeningRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "net_listening", - "params": [] - } -` - -const mockListeningResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": true - } -` - -func TestBaseClient_Listening(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockListeningResponse)), - } - - listening, err := client.Listening(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockListeningRequest, readBody(httpMock.Request)) - assert.True(t, listening) -} - -const mockPeerCountRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "net_peerCount", - "params": [] - } -` - -const mockPeerCountResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_PeerCount(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockPeerCountResponse)), - } - - peerCount, err := client.PeerCount(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockPeerCountRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), peerCount) -} - -const mockProtocolVersionRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_protocolVersion", - "params": [] - } -` - -const mockProtocolVersionResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_ProtocolVersion(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockProtocolVersionResponse)), - } - - protocolVersion, err := client.ProtocolVersion(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockProtocolVersionRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), protocolVersion) -} - -const mockSyncingRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_syncing", - "params": [] - } -` - -const mockSyncingResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "startingBlock": "0x384", - "currentBlock": "0x386", - "highestBlock": "0x454" - } - } -` - -func TestBaseClient_Syncing(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockSyncingResponse)), - } - - syncing, err := client.Syncing(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockSyncingRequest, readBody(httpMock.Request)) - assert.Equal(t, &types.SyncStatus{ - StartingBlock: types.MustBlockNumberFromHex("0x384"), - CurrentBlock: types.MustBlockNumberFromHex("0x386"), - HighestBlock: types.MustBlockNumberFromHex("0x454"), - }, syncing) -} - -const mockNetworkIDRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "net_version", - "params": [] - } -` - -const mockNetworkIDResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_NetworkID(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockNetworkIDResponse)), - } - - networkID, err := client.NetworkID(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockNetworkIDRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), networkID) -} - -const mockChanIDRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_chainId", - "params": [] - } -` - -const mockChanIDResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_ChainID(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockChanIDResponse)), - } - - chainID, err := client.ChainID(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockChanIDRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), chainID) -} - -const mockGasPriceRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_gasPrice", - "params": [] - } -` - -const mockGasPriceResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x09184e72a000" - } -` - -func TestBaseClient_GasPrice(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGasPriceResponse)), - } - - gasPrice, err := client.GasPrice(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockGasPriceRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(10000000000000), gasPrice) -} - -const mockBlockNumberRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_blockNumber", - "params": [] - } -` - -const mockBlockNumberResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_BlockNumber(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockBlockNumberResponse)), - } - - blockNumber, err := client.BlockNumber(context.Background()) - - require.NoError(t, err) - assert.JSONEq(t, mockBlockNumberRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(1), blockNumber) -} - -const mockGetBalanceRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getBalance", - "params": [ - "0x1111111111111111111111111111111111111111", - "latest" - ] - } -` - -const mockGetBalanceResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x0234c8a3397aab58" - } -` - -func TestBaseClient_GetBalance(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetBalanceResponse)), - } - - balance, err := client.GetBalance( - context.Background(), - types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), - types.LatestBlockNumber, - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetBalanceRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(158972490234375000), balance) -} - -const mockGetStorageAtRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getStorageAt", - "params": [ - "0x1111111111111111111111111111111111111111", - "0x2222222222222222222222222222222222222222222222222222222222222222", - "0x1" - ] - } -` - -const mockGetStorageAtResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x3333333333333333333333333333333333333333333333333333333333333333" - } -` - -func TestBaseClient_GetStorageAt(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetStorageAtResponse)), - } - - storage, err := client.GetStorageAt( - context.Background(), - types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), - types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), - types.MustBlockNumberFromHex("0x1"), - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetStorageAtRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x3333333333333333333333333333333333333333333333333333333333333333", types.PadNone), *storage) -} - -const mockGetTransactionCountRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getTransactionCount", - "params": [ - "0x1111111111111111111111111111111111111111", - "0x1" - ] - } -` - -const mockGetTransactionCountResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_GetTransactionCount(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionCountResponse)), - } - - transactionCount, err := client.GetTransactionCount( - context.Background(), - types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), - types.MustBlockNumberFromHex("0x1"), - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetTransactionCountRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), transactionCount) -} - -const mockGetBlockTransactionCountByHashRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getBlockTransactionCountByHash", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111" - ] - } -` - -const mockGetBlockTransactionCountByHashResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_GetBlockTransactionCountByHash(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetBlockTransactionCountByHashResponse)), - } - - transactionCount, err := client.GetBlockTransactionCountByHash( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - ) - require.NoError(t, err) - assert.JSONEq(t, mockGetBlockTransactionCountByHashRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), transactionCount) -} - -const mockGetBlockTransactionCountByNumberRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getBlockTransactionCountByNumber", - "params": [ - "0x1" - ] - } -` - -const mockGetBlockTransactionCountByNumberResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x2" - } -` - -func TestBaseClient_GetBlockTransactionCountByNumber(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetBlockTransactionCountByNumberResponse)), - } - - transactionCount, err := client.GetBlockTransactionCountByNumber( - context.Background(), - types.MustBlockNumberFromHex("0x1"), - ) - require.NoError(t, err) - assert.JSONEq(t, mockGetBlockTransactionCountByNumberRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(2), transactionCount) -} - -const mockGetUncleCountByBlockHashRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getUncleCountByBlockHash", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111" - ] - } -` - -const mockGetUncleCountByBlockHashResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_GetUncleCountByBlockHash(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetUncleCountByBlockHashResponse)), - } - - uncleCount, err := client.GetUncleCountByBlockHash( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - ) - require.NoError(t, err) - assert.JSONEq(t, mockGetUncleCountByBlockHashRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(1), uncleCount) -} - -const mockGetUncleCountByBlockNumberRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getUncleCountByBlockNumber", - "params": [ - "0x1" - ] - } -` - -const mockGetUncleCountByBlockNumberResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x2" - } -` - -func TestBaseClient_GetUncleCountByBlockNumber(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetUncleCountByBlockNumberResponse)), - } - - uncleCount, err := client.GetUncleCountByBlockNumber( - context.Background(), - types.MustBlockNumberFromHex("0x1"), - ) - require.NoError(t, err) - assert.JSONEq(t, mockGetUncleCountByBlockNumberRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(2), uncleCount) -} - -const mockGetCodeRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getCode", - "params": [ - "0x1111111111111111111111111111111111111111", - "0x2" - ] - } -` - -const mockGetCodeResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x3333333333333333333333333333333333333333333333333333333333333333" - } -` - -func TestBaseClient_GetCode(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetCodeResponse)), - } - - code, err := client.GetCode( - context.Background(), - types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), - types.MustBlockNumberFromHex("0x2"), - ) - require.NoError(t, err) - assert.JSONEq(t, mockGetCodeRequest, readBody(httpMock.Request)) - assert.Equal(t, hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333"), code) -} - -const mockSignRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_sign", - "params": [ - "0x1111111111111111111111111111111111111111", - "0x416c6c20796f75722062617365206172652062656c6f6e6720746f207573" - ] - } -` - -const mockSignResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f" - } -` - -func TestBaseClient_Sign(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockSignResponse)), - } - - signature, err := client.Sign( - context.Background(), - types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), - []byte("All your base are belong to us"), - ) - require.NoError(t, err) - assert.JSONEq(t, mockSignRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), *signature) -} - -const mockSignTransactionRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_signTransaction", - "params": [ - { - "from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155", - "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "value": "0x2540be400", - "input": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" - } - ] - } -` - -const mockSignTransactionResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "raw": "0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333", - "tx": { - "nonce": "0x0", - "gasPrice": "0x09184e72a000", - "gas": "0x76c0", - "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", - "value": "0x2540be400", - "input": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675", - "v": "0x11", - "r": "0x2222222222222222222222222222222222222222222222222222222222222222", - "s": "0x3333333333333333333333333333333333333333333333333333333333333333" - } - } - } -` - -func TestBaseClient_SignTransaction(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockSignTransactionResponse)), - } - - from := types.MustAddressFromHex("0xb60e8dd61c5d32be8058bb8eb970870f07233155") - to := types.MustAddressFromHex("0xd46e8dd67c5d32be8058bb8eb970870f07244567") - gasLimit := uint64(30400) - chainID := uint64(1) - raw, tx, err := client.SignTransaction( - context.Background(), - &types.Transaction{ - ChainID: &chainID, - Call: types.Call{ - From: &from, - To: &to, - GasLimit: &gasLimit, - GasPrice: big.NewInt(10000000000000), - Value: big.NewInt(10000000000), - Input: hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"), - }, - }, - ) - require.NoError(t, err) - assert.JSONEq(t, mockSignTransactionRequest, readBody(httpMock.Request)) - assert.Equal(t, hexToBytes("0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333"), raw) - assert.Equal(t, &to, tx.To) - assert.Equal(t, uint64(30400), *tx.GasLimit) - assert.Equal(t, big.NewInt(10000000000000), tx.GasPrice) - assert.Equal(t, big.NewInt(10000000000), tx.Value) - assert.Equal(t, hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"), tx.Input) - assert.Equal(t, uint8(0x11), tx.Signature.Bytes()[64]) - assert.Equal(t, hexToBytes("0x2222222222222222222222222222222222222222222222222222222222222222"), tx.Signature.Bytes()[:32]) - assert.Equal(t, hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333"), tx.Signature.Bytes()[32:64]) -} - -const mockSendTransactionRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_sendTransaction", - "params": [ - { - "from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155", - "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "value": "0x2540be400", - "input": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" - } - ] - } -` - -const mockSendTransactionResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1111111111111111111111111111111111111111111111111111111111111111" - } -` - -func TestBaseClient_SendTransaction(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockSendTransactionResponse)), - } - - from := types.MustAddressFromHex("0xb60e8dd61c5d32be8058bb8eb970870f07233155") - to := types.MustAddressFromHex("0xd46e8dd67c5d32be8058bb8eb970870f07244567") - gasLimit := uint64(30400) - gasPrice := big.NewInt(10000000000000) - value := big.NewInt(10000000000) - input := hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675") - chainID := uint64(1) - txHash, tx, err := client.SendTransaction( - context.Background(), - &types.Transaction{ - ChainID: &chainID, - Call: types.Call{ - From: &from, - To: &to, - GasLimit: &gasLimit, - GasPrice: big.NewInt(10000000000000), - Value: big.NewInt(10000000000), - Input: hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"), - }, - }, - ) - require.NoError(t, err) - assert.JSONEq(t, mockSendTransactionRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), *txHash) - assert.Equal(t, &from, tx.From) - assert.Equal(t, &to, tx.To) - assert.Equal(t, gasLimit, *tx.GasLimit) - assert.Equal(t, gasPrice, tx.GasPrice) - assert.Equal(t, value, tx.Value) - assert.Equal(t, input, tx.Input) -} - -const mockSendRawTransactionRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_sendRawTransaction", - "params": [ - "0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333" - ] - } -` - -const mockSendRawTransactionResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1111111111111111111111111111111111111111111111111111111111111111" - } -` - -func TestBaseClient_SendRawTransaction(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockSendRawTransactionResponse)), - } - - txHash, err := client.SendRawTransaction( - context.Background(), - hexToBytes("0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333"), - ) - require.NoError(t, err) - assert.JSONEq(t, mockSendRawTransactionRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), *txHash) -} - -const mockCallRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_call", - "params": [ - { - "from": "0x1111111111111111111111111111111111111111", - "to": "0x2222222222222222222222222222222222222222", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "value": "0x2540be400", - "data": "0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333" - }, - "0x1" - ] - } -` - -const mockCallResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000d9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f300000000000000000000000078d1ad571a1a09d60d9bbf25894b44e4c8859595000000000000000000000000286834935f4a8cfb4ff4c77d5770c2775ae2b0e7000000000000000000000000b86e2b0ab5a4b1373e40c51a7c712c70ba2f9f8e" - } -` - -func TestBaseClient_Call(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockCallResponse)), - } - - from := types.MustAddressFromHexPtr("0x1111111111111111111111111111111111111111") - to := types.MustAddressFromHexPtr("0x2222222222222222222222222222222222222222") - gasLimit := uint64(30400) - gasPrice := big.NewInt(10000000000000) - value := big.NewInt(10000000000) - input := hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333") - calldata, call, err := client.Call( - context.Background(), - &types.Call{ - From: from, - To: to, - GasLimit: &gasLimit, - GasPrice: gasPrice, - Value: value, - Input: input, - }, - types.MustBlockNumberFromHex("0x1"), - ) - require.NoError(t, err) - assert.JSONEq(t, mockCallRequest, readBody(httpMock.Request)) - assert.Equal(t, hexToBytes("0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000d9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f300000000000000000000000078d1ad571a1a09d60d9bbf25894b44e4c8859595000000000000000000000000286834935f4a8cfb4ff4c77d5770c2775ae2b0e7000000000000000000000000b86e2b0ab5a4b1373e40c51a7c712c70ba2f9f8e"), calldata) - assert.Equal(t, from, call.From) - assert.Equal(t, to, call.To) - assert.Equal(t, gasLimit, *call.GasLimit) - assert.Equal(t, gasPrice, call.GasPrice) - assert.Equal(t, value, call.Value) - assert.Equal(t, input, call.Input) -} - -const mockEstimateGasRequest = ` - { - "id": 1, - "jsonrpc": "2.0", - "method": "eth_estimateGas", - "params": [ - { - "from": "0x1111111111111111111111111111111111111111", - "to": "0x2222222222222222222222222222222222222222", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "value": "0x2540be400", - "data": "0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333" - }, - "latest" - ] - } -` - -const mockEstimateGasResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x5208" - } -` - -func TestBaseClient_EstimateGas(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockEstimateGasResponse)), - } - - gasLimit := uint64(30400) - gas, _, err := client.EstimateGas( - context.Background(), - &types.Call{ - From: types.MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), - To: types.MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), - GasLimit: &gasLimit, - GasPrice: big.NewInt(10000000000000), - Value: big.NewInt(10000000000), - Input: hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333"), - }, - types.LatestBlockNumber, - ) - require.NoError(t, err) - assert.JSONEq(t, mockEstimateGasRequest, readBody(httpMock.Request)) - assert.Equal(t, uint64(21000), gas) -} - -const mockBlockByNumberRequest = ` - { - "method": "eth_getBlockByNumber", - "params": [ - "0x1", - true - ], - "id": 1, - "jsonrpc": "2.0" - } -` - -const mockBlockByNumberResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "number": "0x11", - "hash": "0x2222222222222222222222222222222222222222222222222222222222222222", - "parentHash": "0x3333333333333333333333333333333333333333333333333333333333333333", - "nonce": "0x4444444444444444", - "sha3Uncles": "0x5555555555555555555555555555555555555555555555555555555555555555", - "logsBloom": "0x66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666", - "transactionsRoot": "0x7777777777777777777777777777777777777777777777777777777777777777", - "stateRoot": "0x8888888888888888888888888888888888888888888888888888888888888888", - "receiptsRoot": "0x9999999999999999999999999999999999999999999999999999999999999999", - "miner": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "difficulty": "0xbbbbbb", - "totalDifficulty": "0xcccccc", - "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", - "size": "0xdddddd", - "gasLimit": "0xeeeeee", - "gasUsed": "0xffffff", - "timestamp": "0x54e34e8e", - "transactions": [ - { - "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "nonce": "0x22", - "blockHash": "0x3333333333333333333333333333333333333333333333333333333333333333", - "blockNumber": "0x4444", - "transactionIndex": "0x01", - "from": "0x5555555555555555555555555555555555555555", - "to": "0x6666666666666666666666666666666666666666", - "value": "0x2540be400", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "input": "0x777777777777" - } - ], - "uncles": [ - "0x8888888888888888888888888888888888888888888888888888888888888888" - ] - } - } -` - -func TestBaseClient_BlockByNumber(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockBlockByNumberResponse)), - } - - block, err := client.BlockByNumber( - context.Background(), - types.MustBlockNumberFromHex("0x1"), - true, - ) - require.NoError(t, err) - assert.JSONEq(t, mockBlockByNumberRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(0x11), block.Number) - assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) - assert.Equal(t, types.MustHashFromHex("0x3333333333333333333333333333333333333333333333333333333333333333", types.PadNone), block.ParentHash) - assert.Equal(t, hexToBigInt("0x4444444444444444"), block.Nonce) - assert.Equal(t, types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), block.Sha3Uncles) - assert.Equal(t, hexToBytes("0x66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666"), block.LogsBloom) - assert.Equal(t, types.MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", types.PadNone), block.TransactionsRoot) - assert.Equal(t, types.MustHashFromHex("0x8888888888888888888888888888888888888888888888888888888888888888", types.PadNone), block.StateRoot) - assert.Equal(t, types.MustHashFromHex("0x9999999999999999999999999999999999999999999999999999999999999999", types.PadNone), block.ReceiptsRoot) - assert.Equal(t, types.MustAddressFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), block.Miner) - assert.Equal(t, hexToBigInt("0xbbbbbb"), block.Difficulty) - assert.Equal(t, hexToBigInt("0xcccccc"), block.TotalDifficulty) - assert.Equal(t, hexToBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), block.ExtraData) - assert.Equal(t, hexToBigInt("0xdddddd").Uint64(), block.Size) - assert.Equal(t, hexToBigInt("0xeeeeee").Uint64(), block.GasLimit) - assert.Equal(t, hexToBigInt("0xffffff").Uint64(), block.GasUsed) - assert.Equal(t, int64(1424182926), block.Timestamp.Unix()) - require.Len(t, block.Transactions, 1) - require.Len(t, block.Uncles, 1) - assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), block.Transactions[0].Hash) - assert.Equal(t, uint64(0x22), *block.Transactions[0].Nonce) - assert.Equal(t, types.MustAddressFromHexPtr("0x5555555555555555555555555555555555555555"), block.Transactions[0].From) - assert.Equal(t, types.MustAddressFromHexPtr("0x6666666666666666666666666666666666666666"), block.Transactions[0].To) - assert.Equal(t, big.NewInt(10000000000), block.Transactions[0].Value) - assert.Equal(t, uint64(30400), *block.Transactions[0].GasLimit) - assert.Equal(t, big.NewInt(10000000000000), block.Transactions[0].GasPrice) - assert.Equal(t, hexToBytes("0x777777777777"), block.Transactions[0].Input) - assert.Equal(t, types.MustHashFromHex("0x8888888888888888888888888888888888888888888888888888888888888888", types.PadNone), block.Uncles[0]) -} - -const mockBlockByHashRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getBlockByHash", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111", - true - ] - } -` - -func TestBaseClient_BlockByHash(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockBlockByNumberResponse)), - } - - block, err := client.BlockByHash( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - true, - ) - - require.NoError(t, err) - assert.JSONEq(t, mockBlockByHashRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) -} - -const mockGetTransactionByHashRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getTransactionByHash", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111" - ] - } -` - -const mockGetTransactionByHashResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "blockNumber": "0x22", - "from": "0x3333333333333333333333333333333333333333", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "hash": "0x4444444444444444444444444444444444444444444444444444444444444444", - "input": "0x555555555555", - "nonce": "0x66", - "to": "0x7777777777777777777777777777777777777777", - "transactionIndex": "0x0", - "value": "0x2540be400", - "v": "0x88", - "r": "0x9999999999999999999999999999999999999999999999999999999999999999", - "s": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - } - } -` - -func TestBaseClient_GetTransactionByHash(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionByHashResponse)), - } - - tx, err := client.GetTransactionByHash( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetTransactionByHashRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), tx.BlockHash) - assert.Equal(t, big.NewInt(0x22), tx.BlockNumber) - assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), tx.Hash) - assert.Equal(t, types.MustAddressFromHexPtr("0x3333333333333333333333333333333333333333"), tx.From) - assert.Equal(t, types.MustAddressFromHexPtr("0x7777777777777777777777777777777777777777"), tx.To) - assert.Equal(t, big.NewInt(10000000000), tx.Value) - assert.Equal(t, uint64(30400), *tx.GasLimit) - assert.Equal(t, big.NewInt(10000000000000), tx.GasPrice) - assert.Equal(t, hexToBytes("0x555555555555"), tx.Input) - assert.Equal(t, uint64(0x66), *tx.Nonce) - assert.Equal(t, hexToBigInt("0x0").Uint64(), *tx.TransactionIndex) - assert.Equal(t, uint8(0x88), tx.Signature.Bytes()[64]) - assert.Equal(t, hexToBytes("0x9999999999999999999999999999999999999999999999999999999999999999"), tx.Signature.Bytes()[:32]) - assert.Equal(t, hexToBytes("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), tx.Signature.Bytes()[32:64]) - assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), tx.Hash) -} - -const mockGetTransactionByBlockHashAndIndexRequest = ` - { - "id": 1, - "jsonrpc": "2.0", - "method": "eth_getTransactionByBlockHashAndIndex", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111", - "0x0" - ] - } -` - -func TestBaseClient_GetTransactionByBlockHashAndIndex(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionByHashResponse)), - } - - tx, err := client.GetTransactionByBlockHashAndIndex( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - 0, - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetTransactionByBlockHashAndIndexRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), tx.Hash) -} - -const mockGetTransactionByBlockNumberAndIndexRequest = ` - { - "id": 1, - "jsonrpc": "2.0", - "method": "eth_getTransactionByBlockNumberAndIndex", - "params": [ - "0x1", - "0x2" - ] - } -` - -func TestBaseClient_GetTransactionByBlockNumberAndIndex(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionByHashResponse)), - } - - tx, err := client.GetTransactionByBlockNumberAndIndex( - context.Background(), - types.MustBlockNumberFromHex("0x1"), - 2, - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetTransactionByBlockNumberAndIndexRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), tx.Hash) -} - -const mockGetTransactionReceiptRequest = ` - { - "id": 1, - "jsonrpc": "2.0", - "method": "eth_getTransactionReceipt", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111" - ] - } -` - -const mockGetTransactionReceiptResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "blockNumber": "0x2222", - "contractAddress": null, - "cumulativeGasUsed": "0x33333", - "effectiveGasPrice":"0x4444444444", - "from": "0x5555555555555555555555555555555555555555", - "gasUsed": "0x66666", - "logs": [ - { - "address": "0x7777777777777777777777777777777777777777", - "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "blockNumber": "0x2222", - "data": "0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35", - "logIndex": "0x8", - "removed": false, - "topics": [ - "0x9999999999999999999999999999999999999999999999999999999999999999" - ], - "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "transactionIndex": "0x11" - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000", - "status": "0x1", - "to": "0x7777777777777777777777777777777777777777", - "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "transactionIndex": "0x11", - "type": "0x0" - } - } -` - -func TestBaseClient_GetTransactionReceipt(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionReceiptResponse)), - } - - receipt, err := client.GetTransactionReceipt( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - ) - - status := uint64(1) - require.NoError(t, err) - assert.JSONEq(t, mockGetTransactionReceiptRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipt.TransactionHash) - assert.Equal(t, uint64(17), receipt.TransactionIndex) - assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), receipt.BlockHash) - assert.Equal(t, big.NewInt(0x2222), receipt.BlockNumber) - assert.Equal(t, (*types.Address)(nil), receipt.ContractAddress) - assert.Equal(t, hexToBigInt("0x33333").Uint64(), receipt.CumulativeGasUsed) - assert.Equal(t, hexToBigInt("0x4444444444"), receipt.EffectiveGasPrice) - assert.Equal(t, hexToBigInt("0x66666").Uint64(), receipt.GasUsed) - assert.Equal(t, types.MustAddressFromHex("0x5555555555555555555555555555555555555555"), receipt.From) - assert.Equal(t, types.MustAddressFromHex("0x7777777777777777777777777777777777777777"), receipt.To) - assert.Equal(t, hexToBytes("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000"), receipt.LogsBloom) - assert.Equal(t, &status, receipt.Status) - require.Len(t, receipt.Logs, 1) - assert.Equal(t, types.MustHashFromHexPtr("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipt.Logs[0].TransactionHash) - assert.Equal(t, uint64(17), *receipt.Logs[0].TransactionIndex) - assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), receipt.Logs[0].BlockHash) - assert.Equal(t, big.NewInt(0x2222), receipt.Logs[0].BlockNumber) - assert.Equal(t, uint64(8), *receipt.Logs[0].LogIndex) - assert.Equal(t, hexToBytes("0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35"), receipt.Logs[0].Data) - assert.Equal(t, types.MustAddressFromHex("0x7777777777777777777777777777777777777777"), receipt.Logs[0].Address) - assert.Equal(t, []types.Hash{types.MustHashFromHex("0x9999999999999999999999999999999999999999999999999999999999999999", types.PadNone)}, receipt.Logs[0].Topics) - assert.Equal(t, false, receipt.Logs[0].Removed) -} - -const mockGetBlockReceiptsRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getBlockReceipts", - "params": [ - "0x1" - ] - } -` - -const mockGetBlockReceiptsResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": [ - { - "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "blockNumber": "0x2222", - "contractAddress": null, - "cumulativeGasUsed": "0x33333", - "effectiveGasPrice": "0x4444444444", - "from": "0x5555555555555555555555555555555555555555", - "gasUsed": "0x66666", - "logs": [ - { - "address": "0x7777777777777777777777777777777777777777", - "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "blockNumber": "0x2222", - "data": "0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35", - "logIndex": "0x8", - "removed": false, - "topics": [ - "0x9999999999999999999999999999999999999999999999999999999999999999" - ], - "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "transactionIndex": "0x11" - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000", - "status": "0x1", - "to": "0x7777777777777777777777777777777777777777", - "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "transactionIndex": "0x11", - "type": "0x0" - } - ] - } -` - -func TestBaseClient_GetBlockReceipts(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetBlockReceiptsResponse)), - } - - receipts, err := client.GetBlockReceipts( - context.Background(), - types.MustBlockNumberFromHex("0x1"), - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetBlockReceiptsRequest, readBody(httpMock.Request)) - require.Len(t, receipts, 1) - assert.Equal(t, types.MustHashFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipts[0].TransactionHash) -} - -const mockGetUncleByBlockHashAndIndexRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getUncleByBlockHashAndIndex", - "params": [ - "0x1111111111111111111111111111111111111111111111111111111111111111", - "0x0" - ] - } -` - -func TestBaseClient_GetUncleByBlockHashAndIndex(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockBlockByNumberResponse)), - } - - block, err := client.GetUncleByBlockHashAndIndex( - context.Background(), - types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), - 0, - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetUncleByBlockHashAndIndexRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) -} - -const mockGetUncleByBlockNumberAndIndexRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getUncleByBlockNumberAndIndex", - "params": [ - "0x1", - "0x2" - ] - } -` - -func TestBaseClient_GetUncleByBlockNumberAndIndex(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockBlockByNumberResponse)), - } - - block, err := client.GetUncleByBlockNumberAndIndex( - context.Background(), - types.MustBlockNumberFromHex("0x1"), - 2, - ) - - require.NoError(t, err) - assert.JSONEq(t, mockGetUncleByBlockNumberAndIndexRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) -} - -const mockNewFilterRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_newFilter", - "params": [ - { - "fromBlock": "0x1", - "toBlock": "0x2", - "address": "0x3333333333333333333333333333333333333333", - "topics": ["0x4444444444444444444444444444444444444444444444444444444444444444"] - } - ] - } -` - -const mockNewFilterResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_NewFilter(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockNewFilterResponse)), - } - - from := types.MustBlockNumberFromHex("0x1") - to := types.MustBlockNumberFromHex("0x2") - filterID, err := client.NewFilter(context.Background(), &types.FilterLogsQuery{ - FromBlock: &from, - ToBlock: &to, - Address: []types.Address{types.MustAddressFromHex("0x3333333333333333333333333333333333333333")}, - Topics: [][]types.Hash{ - {types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone)}, - }, - }) - - require.NoError(t, err) - assert.JSONEq(t, mockNewFilterRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(1), filterID) -} - -const mockNewBlockFilterRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_newBlockFilter", - "params": [] - } -` - -const mockNewBlockFilterResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_NewBlockFilter(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockNewBlockFilterResponse)), - } - - filterID, err := client.NewBlockFilter(context.Background()) - - require.NoError(t, err) - assert.JSONEq(t, mockNewBlockFilterRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(1), filterID) -} - -const mockNewPendingTransactionFilterRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_newPendingTransactionFilter", - "params": [] - } -` - -const mockNewPendingTransactionFilterResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_NewPendingTransactionFilter(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockNewPendingTransactionFilterResponse)), - } - - filterID, err := client.NewPendingTransactionFilter(context.Background()) - - require.NoError(t, err) - assert.JSONEq(t, mockNewPendingTransactionFilterRequest, readBody(httpMock.Request)) - assert.Equal(t, big.NewInt(1), filterID) -} - -const mockUninstallFilterRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_uninstallFilter", - "params": ["0x1"] - } -` - -const mockUninstallFilterResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": true - } -` - -func TestBaseClient_UninstallFilter(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockUninstallFilterResponse)), - } - - filterID := big.NewInt(1) - success, err := client.UninstallFilter(context.Background(), filterID) - - require.NoError(t, err) - assert.JSONEq(t, mockUninstallFilterRequest, readBody(httpMock.Request)) - assert.True(t, success) -} - -const mockGetFilterChangesRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getFilterChanges", - "params": ["0x1"] - } -` - -const mockGetFilterChangesResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": [ - { - "address": "0x1111111111111111111111111111111111111111", - "topics": ["0x2222222222222222222222222222222222222222222222222222222222222222"], - "data": "0x3333333333333333333333333333333333333333333333333333333333333333", - "blockNumber": "0x44444", - "transactionHash": "0x5555555555555555555555555555555555555555555555555555555555555555", - "transactionIndex": "0x66", - "blockHash": "0x7777777777777777777777777777777777777777777777777777777777777777", - "logIndex": "0x88", - "removed": false - } - ] - } -` - -func TestBaseClient_GetFilterChanges(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetFilterChangesResponse)), - } - - filterID := big.NewInt(1) - logs, err := client.GetFilterChanges(context.Background(), filterID) - - require.NoError(t, err) - assert.JSONEq(t, mockGetFilterChangesRequest, readBody(httpMock.Request)) - assert.Len(t, logs, 1) - assert.Equal(t, types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), logs[0].Address) - assert.Equal(t, []types.Hash{types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone)}, logs[0].Topics) - assert.Equal(t, hexutil.MustHexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333"), logs[0].Data) - assert.Equal(t, types.MustBlockNumberFromHexPtr("0x44444").Big(), logs[0].BlockNumber) - assert.Equal(t, types.MustHashFromHexPtr("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), logs[0].TransactionHash) - assert.Equal(t, uint64(0x66), *logs[0].TransactionIndex) - assert.Equal(t, types.MustHashFromHexPtr("0x7777777777777777777777777777777777777777777777777777777777777777", types.PadNone), logs[0].BlockHash) - assert.Equal(t, uint64(0x88), *logs[0].LogIndex) - assert.False(t, logs[0].Removed) -} - -const mockGetBlockFilterChangesRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getFilterChanges", - "params": ["0x1"] - } -` - -const mockGetBlockFilterChangesResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": ["0x1111111111111111111111111111111111111111111111111111111111111111"] - } -` - -func TestBaseClient_GetBlockFilterChanges(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetBlockFilterChangesResponse)), - } - - filterID := big.NewInt(1) - blockHashes, err := client.GetBlockFilterChanges(context.Background(), filterID) - - require.NoError(t, err) - assert.JSONEq(t, mockGetBlockFilterChangesRequest, readBody(httpMock.Request)) - assert.Len(t, blockHashes, 1) - assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), blockHashes[0]) -} - -const mockGetFilterLogsRequest = ` -{ - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getFilterLogs", - "params": ["0x1"] -} -` - -const mockGetFilterLogsResponse = ` -{ - "jsonrpc": "2.0", - "id": 1, - "result": [ - { - "address": "0x1111111111111111111111111111111111111111", - "topics": ["0x2222222222222222222222222222222222222222222222222222222222222222"], - "data": "0x3333333333333333333333333333333333333333333333333333333333333333", - "blockNumber": "0x1", - "transactionHash": "0x5555555555555555555555555555555555555555555555555555555555555555", - "transactionIndex": "0x0", - "blockHash": "0x7777777777777777777777777777777777777777777777777777777777777777", - "logIndex": "0x1", - "removed": false - } - ] -} -` - -func TestBaseClient_GetFilterLogs(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetFilterLogsResponse)), - } - - filterID := big.NewInt(1) - logs, err := client.GetFilterLogs(context.Background(), filterID) - - require.NoError(t, err) - assert.JSONEq(t, mockGetFilterLogsRequest, readBody(httpMock.Request)) - assert.Len(t, logs, 1) - assert.Equal(t, types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), logs[0].Address) - assert.Equal(t, []types.Hash{types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone)}, logs[0].Topics) - assert.Equal(t, hexutil.MustHexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333"), logs[0].Data) - assert.Equal(t, big.NewInt(1), logs[0].BlockNumber) - assert.Equal(t, types.MustHashFromHexPtr("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), logs[0].TransactionHash) - assert.Equal(t, uint64(0), *logs[0].TransactionIndex) - assert.Equal(t, types.MustHashFromHexPtr("0x7777777777777777777777777777777777777777777777777777777777777777", types.PadNone), logs[0].BlockHash) - assert.Equal(t, uint64(1), *logs[0].LogIndex) - assert.False(t, logs[0].Removed) -} - -const mockGetLogsRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_getLogs", - "params": [ - { - "fromBlock": "0x1", - "toBlock": "0x2", - "address": "0x3333333333333333333333333333333333333333", - "topics": [ - "0x4444444444444444444444444444444444444444444444444444444444444444" - ] - } - ] - } -` - -const mockGetLogsResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": [ - { - "address": "0x3333333333333333333333333333333333333333", - "topics": [ - "0x4444444444444444444444444444444444444444444444444444444444444444" - ], - "data": "0x68656c6c6f21", - "blockNumber": "0x1", - "transactionHash": "0x4444444444444444444444444444444444444444444444444444444444444444", - "transactionIndex": "0x0", - "blockHash": "0x4444444444444444444444444444444444444444444444444444444444444444", - "logIndex": "0x0", - "removed": false - } - ] - } -` - -func TestBaseClient_GetLogs(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockGetLogsResponse)), - } - - from := types.MustBlockNumberFromHex("0x1") - to := types.MustBlockNumberFromHex("0x2") - logs, err := client.GetLogs(context.Background(), &types.FilterLogsQuery{ - FromBlock: &from, - ToBlock: &to, - Address: []types.Address{types.MustAddressFromHex("0x3333333333333333333333333333333333333333")}, - Topics: [][]types.Hash{ - {types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone)}, - }, - }) - require.NoError(t, err) - assert.JSONEq(t, mockGetLogsRequest, readBody(httpMock.Request)) - require.Len(t, logs, 1) - assert.Equal(t, types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), logs[0].Address) - assert.Equal(t, types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].Topics[0]) - assert.Equal(t, hexToBytes("0x68656c6c6f21"), logs[0].Data) - assert.Equal(t, big.NewInt(1), logs[0].BlockNumber) - assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].TransactionHash) - assert.Equal(t, uint64(0), *logs[0].TransactionIndex) - assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].BlockHash) - assert.Equal(t, uint64(0), *logs[0].LogIndex) - assert.Equal(t, false, logs[0].Removed) -} - -const mockMaxPriorityFeePerGasRequest = ` - { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_maxPriorityFeePerGas", - "params": [] - } -` - -const mockMaxPriorityFeePerGasResponse = ` - { - "jsonrpc": "2.0", - "id": 1, - "result": "0x1" - } -` - -func TestBaseClient_MaxPriorityFeePerGas(t *testing.T) { - httpMock := newHTTPMock() - client := &baseClient{transport: httpMock} - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockMaxPriorityFeePerGasResponse)), - } - - gasPrice, err := client.MaxPriorityFeePerGas(context.Background()) - require.NoError(t, err) - assert.JSONEq(t, mockMaxPriorityFeePerGasRequest, readBody(httpMock.Request)) - assert.Equal(t, hexToBigInt("0x1"), gasPrice) -} - -const mockSubscribeLogsResponse = ` - { - "address": "0x3333333333333333333333333333333333333333", - "topics": [ - "0x4444444444444444444444444444444444444444444444444444444444444444" - ], - "data": "0x68656c6c6f21", - "blockNumber": "0x1", - "transactionHash": "0x4444444444444444444444444444444444444444444444444444444444444444", - "transactionIndex": "0x0", - "blockHash": "0x4444444444444444444444444444444444444444444444444444444444444444", - "logIndex": "0x0", - "removed": false - } -` - -func TestBaseClient_SubscribeLogs(t *testing.T) { - streamMock := newStreamMock(t) - client := &baseClient{transport: streamMock} - - // Mock subscribe response - rawCh := make(chan json.RawMessage) - query := &types.FilterLogsQuery{ - FromBlock: types.BlockNumberFromUint64Ptr(1), - ToBlock: types.BlockNumberFromUint64Ptr(2), - Address: []types.Address{types.MustAddressFromHex("0x3333333333333333333333333333333333333333")}, - Topics: [][]types.Hash{ - {types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone)}, - }, - } - streamMock.SubscribeMocks = append(streamMock.SubscribeMocks, subscribeMock{ - ArgMethod: "logs", - ArgParams: []any{query}, - RetCh: rawCh, - RetID: "1", - RetErr: nil, - }) - streamMock.UnsubscribeMocks = append(streamMock.UnsubscribeMocks, unsubscribeMock{ - ArgID: "1", - }) - - // Subscribe - ctx, ctxCancel := context.WithCancel(context.Background()) - defer ctxCancel() - logsCh, err := client.SubscribeLogs(ctx, query) - - // Assert subscribe response - require.NotNil(t, logsCh) - require.NoError(t, err) - - // Mock response - rawCh <- json.RawMessage(mockSubscribeLogsResponse) - - // Assert received log - logs := <-logsCh - require.NotNil(t, logs) - assert.Equal(t, "0x3333333333333333333333333333333333333333", logs.Address.String()) - assert.Equal(t, "0x4444444444444444444444444444444444444444444444444444444444444444", logs.Topics[0].String()) - assert.Equal(t, "0x68656c6c6f21", hexutil.BytesToHex(logs.Data)) - assert.Equal(t, "1", logs.BlockNumber.String()) - assert.Equal(t, "0x4444444444444444444444444444444444444444444444444444444444444444", logs.TransactionHash.String()) - assert.Equal(t, uint64(0), *logs.TransactionIndex) - assert.Equal(t, "0x4444444444444444444444444444444444444444444444444444444444444444", logs.BlockHash.String()) - assert.Equal(t, uint64(0), *logs.LogIndex) - assert.Equal(t, false, logs.Removed) - - ctxCancel() - assert.Eventually(t, func() bool { - return len(streamMock.UnsubscribeMocks) == 0 - }, time.Second, 10*time.Millisecond) -} - -const mockSubscribeNewHeadsResponse = ` - { - "number": "0x11", - "hash": "0x2222222222222222222222222222222222222222222222222222222222222222", - "parentHash": "0x3333333333333333333333333333333333333333333333333333333333333333", - "nonce": "0x4444444444444444", - "sha3Uncles": "0x5555555555555555555555555555555555555555555555555555555555555555", - "logsBloom": "0x66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666", - "transactionsRoot": "0x7777777777777777777777777777777777777777777777777777777777777777", - "stateRoot": "0x8888888888888888888888888888888888888888888888888888888888888888", - "receiptsRoot": "0x9999999999999999999999999999999999999999999999999999999999999999", - "miner": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "difficulty": "0xbbbbbb", - "totalDifficulty": "0xcccccc", - "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", - "size": "0xdddddd", - "gasLimit": "0xeeeeee", - "gasUsed": "0xffffff", - "timestamp": "0x54e34e8e", - "transactions": [ - { - "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", - "nonce": "0x22", - "blockHash": "0x3333333333333333333333333333333333333333333333333333333333333333", - "blockNumber": "0x4444", - "transactionIndex": "0x01", - "from": "0x5555555555555555555555555555555555555555", - "to": "0x6666666666666666666666666666666666666666", - "value": "0x2540be400", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "input": "0x777777777777" - } - ], - "uncles": [ - "0x8888888888888888888888888888888888888888888888888888888888888888" - ] - } -` - -func TestBaseClient_SubscribeNewHeads(t *testing.T) { - streamMock := newStreamMock(t) - client := &baseClient{transport: streamMock} - - // Mock subscribe response - rawCh := make(chan json.RawMessage) - streamMock.SubscribeMocks = append(streamMock.SubscribeMocks, subscribeMock{ - ArgMethod: "newHeads", - ArgParams: []any{}, - RetCh: rawCh, - RetID: "1", - RetErr: nil, - }) - streamMock.UnsubscribeMocks = append(streamMock.UnsubscribeMocks, unsubscribeMock{ - ArgID: "1", - }) - - // Subscribe - ctx, ctxCancel := context.WithCancel(context.Background()) - defer ctxCancel() - headsCh, err := client.SubscribeNewHeads(ctx) - - // Assert subscribe response - require.NotNil(t, headsCh) - require.NoError(t, err) - - // Mock response - rawCh <- json.RawMessage(mockSubscribeNewHeadsResponse) - - // Assert received block - block := <-headsCh - require.NotNil(t, block) - assert.Equal(t, big.NewInt(0x11), block.Number) - assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) - assert.Equal(t, types.MustHashFromHex("0x3333333333333333333333333333333333333333333333333333333333333333", types.PadNone), block.ParentHash) - assert.Equal(t, hexToBigInt("0x4444444444444444"), block.Nonce) - assert.Equal(t, types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), block.Sha3Uncles) - assert.Equal(t, hexToBytes("0x66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666"), block.LogsBloom) - assert.Equal(t, types.MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", types.PadNone), block.TransactionsRoot) - assert.Equal(t, types.MustHashFromHex("0x8888888888888888888888888888888888888888888888888888888888888888", types.PadNone), block.StateRoot) - assert.Equal(t, types.MustHashFromHex("0x9999999999999999999999999999999999999999999999999999999999999999", types.PadNone), block.ReceiptsRoot) - assert.Equal(t, types.MustAddressFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), block.Miner) - assert.Equal(t, hexToBigInt("0xbbbbbb"), block.Difficulty) - assert.Equal(t, hexToBigInt("0xcccccc"), block.TotalDifficulty) - assert.Equal(t, hexToBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), block.ExtraData) - assert.Equal(t, hexToBigInt("0xdddddd").Uint64(), block.Size) - assert.Equal(t, hexToBigInt("0xeeeeee").Uint64(), block.GasLimit) - assert.Equal(t, hexToBigInt("0xffffff").Uint64(), block.GasUsed) - assert.Equal(t, int64(1424182926), block.Timestamp.Unix()) - require.Len(t, block.Transactions, 1) - require.Len(t, block.Uncles, 1) - assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), block.Transactions[0].Hash) - assert.Equal(t, uint64(0x22), *block.Transactions[0].Nonce) - assert.Equal(t, types.MustAddressFromHexPtr("0x5555555555555555555555555555555555555555"), block.Transactions[0].From) - assert.Equal(t, types.MustAddressFromHexPtr("0x6666666666666666666666666666666666666666"), block.Transactions[0].To) - assert.Equal(t, big.NewInt(10000000000), block.Transactions[0].Value) - assert.Equal(t, uint64(30400), *block.Transactions[0].GasLimit) - assert.Equal(t, big.NewInt(10000000000000), block.Transactions[0].GasPrice) - assert.Equal(t, hexToBytes("0x777777777777"), block.Transactions[0].Input) - assert.Equal(t, types.MustHashFromHex("0x8888888888888888888888888888888888888888888888888888888888888888", types.PadNone), block.Uncles[0]) - - ctxCancel() - assert.Eventually(t, func() bool { - return len(streamMock.UnsubscribeMocks) == 0 - }, time.Second, 10*time.Millisecond) -} - -const mockSubscribeNewPendingTransactions = `"0x1111111111111111111111111111111111111111111111111111111111111111"` - -func TestClient_SubscribeNewPendingTransactions(t *testing.T) { - streamMock := newStreamMock(t) - client := &baseClient{transport: streamMock} - - // Mock subscribe response - rawCh := make(chan json.RawMessage) - streamMock.SubscribeMocks = append(streamMock.SubscribeMocks, subscribeMock{ - ArgMethod: "newPendingTransactions", - ArgParams: []any{}, - RetCh: rawCh, - RetID: "1", - RetErr: nil, - }) - streamMock.UnsubscribeMocks = append(streamMock.UnsubscribeMocks, unsubscribeMock{ - ArgID: "1", - }) - - // Subscribe to logs - ctx, ctxCancel := context.WithCancel(context.Background()) - defer ctxCancel() - txCh, err := client.SubscribeNewPendingTransactions(ctx) - - // Assert subscribe request - require.NotNil(t, txCh) - require.NoError(t, err) - - // Mock response - rawCh <- json.RawMessage(mockSubscribeNewPendingTransactions) - - // Assert response - tx := <-txCh - assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), tx) - - ctxCancel() - assert.Eventually(t, func() bool { - return len(streamMock.UnsubscribeMocks) == 0 - }, time.Second, 10*time.Millisecond) -} - -func readBody(r *http.Request) string { - body, _ := io.ReadAll(r.Body) - return string(body) -} - -func hexToBytes(s string) []byte { - b, _ := hexutil.HexToBytes(s) - return b -} - -func hexToBigInt(s string) *big.Int { - b, _ := hexutil.HexToBigInt(s) - return b -} diff --git a/rpc/client.go b/rpc/client.go index 5737513..2c39406 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -1,227 +1,413 @@ package rpc import ( - "context" "fmt" + "math/big" + "reflect" + "sort" "github.com/defiweb/go-eth/rpc/transport" "github.com/defiweb/go-eth/types" "github.com/defiweb/go-eth/wallet" ) -// Client allows to interact with the Ethereum node. +// Client is a default RPC client that provides access to the standard Ethereum +// JSON-RPC APIs. type Client struct { - baseClient - - keys map[types.Address]wallet.Key - defaultAddr *types.Address - txModifiers []TXModifier + MethodsCommon + MethodsFilter + MethodsWallet + MethodsClient } -type ClientOptions func(c *Client) error +type ClientOptionsContext struct { + Transport transport.Transport // Transport instance that will be passed to the client. + Decoder types.TransactionDecoder // Transaction decoder that will be passed to the client. + Custom map[any]any // Custom data that may be used by client options. +} -// TXModifier allows to modify the transaction before it is signed or sent to -// the node. -type TXModifier interface { - Modify(ctx context.Context, client RPC, tx *types.Transaction) error +type ClientOption interface { + Apply(cfg *ClientOptionsContext, client any) error + Order() int } -type TXModifierFunc func(ctx context.Context, client RPC, tx *types.Transaction) error +// WithTransport sets the transport for the client. +func WithTransport(t transport.Transport) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = t + return nil + }, + } +} -func (f TXModifierFunc) Modify(ctx context.Context, client RPC, tx *types.Transaction) error { - return f(ctx, client, tx) +// WithTransactionDecoder sets the transaction decoder for the client. +// The default decoder is types.DefaultTransactionDecoder. +// +// Using custom decoder allows to decode custom transaction types that may be +// present in some L2 implementations. +func WithTransactionDecoder(decoder types.TransactionDecoder) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Decoder = decoder + return nil + }, + } } -// WithTransport sets the transport for the client. -func WithTransport(transport transport.Transport) ClientOptions { - return func(c *Client) error { - c.transport = transport - return nil +// WithPostHijackers adds hijackers that are applied after all other hijackers +// applied by the client. +func WithPostHijackers(hijackers ...transport.Hijacker) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, hijackers...) + return nil + }, + order: 100, } } -// WithKeys allows to set keys that will be used to sign data. -// It allows to emulate the behavior of the RPC methods that require a key. +// WithKeys allows to emulate the behavior of the RPC methods that require +// a private key to sign the data. // // The following methods are affected: // - Accounts - returns the addresses of the provided keys // - Sign - signs the data with the provided key -// - SignTransaction - signs transaction with the provided key -// - SendTransaction - signs transaction with the provided key and sends it -// using SendRawTransaction -func WithKeys(keys ...wallet.Key) ClientOptions { - return func(c *Client) error { - for _, k := range keys { - c.keys[k.Address()] = k - } - return nil +// - SignTransaction - signs transaction +// - SendTransaction - signs transaction and sends it using SendRawTransaction +// +// This option will modify the provided transaction instance. +func WithKeys(keys ...wallet.Key) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, &hijackSign{keys: keys}) + return nil + }, + order: 200, } } -// WithDefaultAddress sets the call "from" address if it is not set in the -// following methods: -// - SignTransaction -// - SendTransaction -// - Call -// - EstimateGas -func WithDefaultAddress(addr types.Address) ClientOptions { - return func(c *Client) error { - c.defaultAddr = &addr - return nil +// WithSimulate simulates the transaction, by calling eth_call with the same +// parameters before sending the transaction. +// +// It works with eth_sendTransaction, eth_sendRawTransaction, and +// eth_sendPrivateTransaction methods. +func WithSimulate() ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, &hijackSimulate{}) + return nil + }, + order: 300, } } -// WithTXModifiers allows to modify the transaction before it is signed and -// sent to the node. +type NonceOptions struct { + UsePendingBlock bool // UsePendingBlock indicates whether to use the pending block. + Replace bool // Replace is true if the nonce should be replaced even if it is already set. +} + +// WithNonce sets the nonce in the transaction. // -// Modifiers will be applied in the order they are provided. -func WithTXModifiers(modifiers ...TXModifier) ClientOptions { - return func(c *Client) error { - c.txModifiers = append(c.txModifiers, modifiers...) - return nil +// This option will modify the provided transaction instance. +func WithNonce(opts NonceOptions) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, &hijackNonce{ + usePendingBlock: opts.UsePendingBlock, + replace: opts.Replace, + }) + return nil + }, + order: 400, } } -// NewClient creates a new RPC client. -// The WithTransport option is required. -func NewClient(opts ...ClientOptions) (*Client, error) { - c := &Client{keys: make(map[types.Address]wallet.Key)} - for _, opt := range opts { - if err := opt(c); err != nil { - return nil, err - } - } - if c.transport == nil { - return nil, fmt.Errorf("rpc client: transport is required") - } - return c, nil +type LegacyGasFeeOptions struct { + Multiplier float64 // Multiplier is applied to the gas price. + MinGasPrice *big.Int // MinGasPrice is the minimum gas price, or nil if there is no lower bound. + MaxGasPrice *big.Int // MaxGasPrice is the maximum gas price, or nil if there is no upper bound. + Replace bool // Replace is true if the gas price should be replaced even if it is already set. + AllowChangeType bool // AllowChangeType is true if the transaction type can be changed if it does not support legacy price data. } -// Accounts implements the RPC interface. -func (c *Client) Accounts(ctx context.Context) ([]types.Address, error) { - if len(c.keys) > 0 { - var res []types.Address - for _, key := range c.keys { - res = append(res, key.Address()) - } - return res, nil +// WithLegacyGasFee estimates the gas price and sets it in the transaction. +// +// It only works with eth_sendTransaction method, raw transactions are not supported. +// +// This option will modify the provided transaction instance. +func WithLegacyGasFee(opts LegacyGasFeeOptions) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + if opts.Multiplier == 0 { + return fmt.Errorf("rpc client: gas price multiplier must be greater than 0") + } + ctx.Transport = addHijacker(ctx.Transport, &hijackLegacyGasFee{ + multiplier: opts.Multiplier, + minGasPrice: opts.MinGasPrice, + maxGasPrice: opts.MaxGasPrice, + replace: opts.Replace, + allowChangeType: opts.AllowChangeType, + }) + return nil + }, + order: 500, } - return c.baseClient.Accounts(ctx) } -// Sign implements the RPC interface. -func (c *Client) Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) { - if len(c.keys) == 0 { - return c.baseClient.Sign(ctx, account, data) +type DynamicGasFeeOptions struct { + GasPriceMultiplier float64 // GasPriceMultiplier is applied to the gas price. + PriorityFeePerGasMultiplier float64 // PriorityFeePerGasMultiplier is applied to the priority fee per gas. + MinGasPrice *big.Int // MinGasPrice is the minimum gas price, or nil if there is no lower bound. + MaxGasPrice *big.Int // MaxGasPrice is the maximum gas price, or nil if there is no upper bound. + MinPriorityFeePerGas *big.Int // MinPriorityFeePerGas is the minimum priority fee per gas, or nil if there is no lower bound. + MaxPriorityFeePerGas *big.Int // MaxPriorityFeePerGas is the maximum priority fee per gas, or nil if there is no upper bound. + Replace bool // Replace is true if the gas price should be replaced even if it is already set. + AllowChangeType bool // AllowChangeType is true if the transaction type can be changed if it does not support dynamic price data. +} + +// WithDynamicGasFee estimates the gas price and sets it in the transaction. +// +// It only works with eth_sendTransaction method, raw transactions are not supported. +// +// This option will modify the provided transaction instance. +func WithDynamicGasFee(opts DynamicGasFeeOptions) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + if opts.GasPriceMultiplier == 0 || opts.PriorityFeePerGasMultiplier == 0 { + return fmt.Errorf("rpc client: gas price and priority fee multipliers must be greater than 0") + } + ctx.Transport = addHijacker(ctx.Transport, &hijackDynamicGasFee{ + gasPriceMultiplier: opts.GasPriceMultiplier, + priorityFeePerGasMultiplier: opts.PriorityFeePerGasMultiplier, + minGasPrice: opts.MinGasPrice, + maxGasPrice: opts.MaxGasPrice, + minPriorityFeePerGas: opts.MinPriorityFeePerGas, + maxPriorityFeePerGas: opts.MaxPriorityFeePerGas, + replace: opts.Replace, + allowChangeType: opts.AllowChangeType, + }) + return nil + }, + order: 600, } - if key := c.findKey(&account); key != nil { - return key.SignMessage(ctx, data) +} + +type GasLimitOptions struct { + Multiplier float64 // Multiplier is applied to the gas limit. + MinGas uint64 // MinGas is the minimum gas limit, or 0 if there is no lower bound. + MaxGas uint64 // MaxGas is the maximum gas limit, or 0 if there is no upper bound. + Replace bool // Replace is true if the gas limit should be replaced even if it is already set. +} + +// WithGasLimit estimates the gas limit and sets it in the transaction. +// +// This option will modify the provided transaction instance. +func WithGasLimit(opts GasLimitOptions) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + if opts.Multiplier == 0 { + return fmt.Errorf("rpc client: gas limit multiplier must be greater than 0") + } + ctx.Transport = addHijacker(ctx.Transport, &hijackGasLimit{ + multiplier: opts.Multiplier, + minGas: opts.MinGas, + maxGas: opts.MaxGas, + replace: opts.Replace, + }) + return nil + }, + order: 700, } - return nil, fmt.Errorf("rpc client: no key found for address %s", account) } -// SignTransaction implements the RPC interface. -func (c *Client) SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, *types.Transaction, error) { - tx, err := c.PrepareTransaction(ctx, tx) - if err != nil { - return nil, nil, err +type AddressOptions struct { + Address types.Address // Address is the default address to use. + Replace bool // Replace is true if the address should be replaced even if it is already set. +} + +// WithDefaultAddress sets the default address for calls and transactions. +// +// To send a call with to a zero address, it must be set explicitly in the call. +// +// This option will modify the provided transaction and call instances. +func WithDefaultAddress(opts AddressOptions) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, &hijackAddress{ + address: opts.Address, + replace: opts.Replace, + }) + return nil + }, + order: 800, } - if len(c.keys) == 0 { - return c.baseClient.SignTransaction(ctx, tx) +} + +type ChainIDOptions struct { + ChainID uint64 // ChainID is the chain ID to use. If 0, then value is fetched from the node. + Replace bool // Replace is true if the chain ID should be replaced even if it is already set. +} + +// WithChainID sets the chain ID in the transaction. +// It only works with eth_sendTransaction method. +// +// This option will modify the provided transaction instance. +func WithChainID(opts ChainIDOptions) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, &hijackChainID{ + chainID: opts.ChainID, + replace: opts.Replace, + }) + return nil + }, + order: 900, } - if key := c.findKey(tx.Call.From); key != nil { - if err := key.SignTransaction(ctx, tx); err != nil { - return nil, nil, err - } - raw, err := tx.Raw() - if err != nil { - return nil, nil, err - } - return raw, tx, nil +} + +// WithPreHijackers adds hijackers that are applied before any other hijackers +// applied by the client. +func WithPreHijackers(hijackers ...transport.Hijacker) ClientOption { + return &option{ + apply: func(ctx *ClientOptionsContext, _ any) error { + ctx.Transport = addHijacker(ctx.Transport, hijackers...) + return nil + }, + order: 1000, } - return nil, nil, fmt.Errorf("rpc client: no key found for address %s", tx.Call.From) } -// SendTransaction implements the RPC interface. -func (c *Client) SendTransaction(ctx context.Context, tx *types.Transaction) (*types.Hash, *types.Transaction, error) { - tx, err := c.PrepareTransaction(ctx, tx) - if err != nil { - return nil, nil, err +type ClientOptions func(c *ClientOptionsContext, client any) error + +// NewClient creates a new RPC client. +// +// The WithTransport option is required. +func NewClient(opts ...ClientOption) (*Client, error) { + ctx := &ClientOptionsContext{} + client := &Client{} + if err := applyOptions(ctx, opts); err != nil { + return nil, fmt.Errorf("rpc client: option error: %w", err) } - if len(c.keys) == 0 { - return c.baseClient.SendTransaction(ctx, tx) + if ctx.Decoder == nil { + ctx.Decoder = types.DefaultTransactionDecoder } - if key := c.findKey(tx.Call.From); key != nil { - if err := key.SignTransaction(ctx, tx); err != nil { - return nil, nil, err - } - raw, err := tx.Raw() - if err != nil { - return nil, nil, err - } - txHash, err := c.SendRawTransaction(ctx, raw) - if err != nil { - return nil, nil, err - } - return txHash, tx, nil + if ctx.Transport == nil { + return nil, fmt.Errorf("rpc client: transport is required") } - return nil, nil, fmt.Errorf("rpc client: no key found for address %s", tx.Call.From) + client.MethodsCommon.Transport = ctx.Transport + client.MethodsFilter.Transport = ctx.Transport + client.MethodsWallet.Transport = ctx.Transport + client.MethodsClient.Transport = ctx.Transport + client.MethodsCommon.Decoder = ctx.Decoder + return client, nil } -// PrepareTransaction prepares the transaction by applying transaction -// modifiers and setting the default address if it is not set. +// NewCustomClient returns a new custom client. A custom client may implement +// additional methods that are not part of the standard client. // -// A copy of the modified transaction is returned. -func (c *Client) PrepareTransaction(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) { - if tx == nil { - return nil, fmt.Errorf("rpc client: transaction is nil") +// The WithTransport option is required. +// +// This method automatically initializes the client fields that are nil +// and recursively sets the all fields that are of the transport.Transport or +// types.TransactionDecoder types. +func NewCustomClient[T any](opts ...ClientOption) (*T, error) { + ctx := &ClientOptionsContext{} + client := new(T) + if err := applyOptions(ctx, opts); err != nil { + return nil, fmt.Errorf("rpc client: option error: %w", err) } - txCpy := tx.Copy() - if txCpy.Call.From == nil && c.defaultAddr != nil { - defaultAddr := *c.defaultAddr - txCpy.Call.From = &defaultAddr + if ctx.Decoder == nil { + ctx.Decoder = types.DefaultTransactionDecoder + } + if ctx.Transport == nil { + return nil, fmt.Errorf("rpc client: transport is required") } - for _, modifier := range c.txModifiers { - if err := modifier.Modify(ctx, c, txCpy); err != nil { - return nil, err + setFields(ctx, reflect.ValueOf(client)) + return client, nil +} + +type option struct { + apply func(*ClientOptionsContext, any) error + order int +} + +func (o *option) Apply(ctx *ClientOptionsContext, client any) error { + return o.apply(ctx, client) +} + +func (o *option) Order() int { + return o.order +} + +func applyOptions(c *ClientOptionsContext, opts []ClientOption) error { + sort.Slice(opts, func(i, j int) bool { + return opts[i].Order() < opts[j].Order() + }) + for _, opt := range opts { + if err := opt.Apply(c, nil); err != nil { + return err } } - return txCpy, nil + return nil } -// Call implements the RPC interface. -func (c *Client) Call(ctx context.Context, call *types.Call, block types.BlockNumber) ([]byte, *types.Call, error) { - if call == nil { - return nil, nil, fmt.Errorf("rpc client: call is nil") +func setFields(ctx *ClientOptionsContext, r reflect.Value) { + for r.Kind() == reflect.Ptr || r.Kind() == reflect.Interface { + r = r.Elem() } - callCpy := call.Copy() - if callCpy.From == nil && c.defaultAddr != nil { - defaultAddr := *c.defaultAddr - callCpy.From = &defaultAddr + if r.Kind() != reflect.Struct { + return + } + for n := 0; n < r.NumField(); n++ { + f := r.Field(n) + if !f.CanInterface() { + continue + } + t := f.Type() + switch t { + case transportTy: + if f.CanSet() { + f.Set(reflect.ValueOf(ctx.Transport)) + } + case decoderTy: + if f.CanSet() { + f.Set(reflect.ValueOf(ctx.Decoder)) + } + default: + if initPtr(f) { + setFields(ctx, f) + } + } } - return c.baseClient.Call(ctx, callCpy, block) } -// EstimateGas implements the RPC interface. -func (c *Client) EstimateGas(ctx context.Context, call *types.Call, block types.BlockNumber) (uint64, *types.Call, error) { - if call == nil { - return 0, nil, fmt.Errorf("rpc client: call is nil") +func initPtr(r reflect.Value) bool { + if !r.CanInterface() { + return false + } + if r.Kind() != reflect.Ptr { + return true + } + if !r.IsNil() { + return true } - callCpy := call.Copy() - if callCpy.From == nil && c.defaultAddr != nil { - defaultAddr := *c.defaultAddr - callCpy.From = &defaultAddr + if r.CanSet() { + r.Set(reflect.New(r.Type().Elem())) + return true } - return c.baseClient.EstimateGas(ctx, callCpy, block) + return false } -// findKey finds a key by address. -func (c *Client) findKey(addr *types.Address) wallet.Key { - if addr == nil { - return nil +func addHijacker(t transport.Transport, hijackers ...transport.Hijacker) transport.Transport { + if h, ok := t.(*transport.Hijack); ok { + h.Use(hijackers...) + return h } - if key, ok := c.keys[*addr]; ok { - return key - } - return nil + return transport.NewHijacker(t, hijackers...) } + +var ( + transportTy = reflect.TypeOf((*transport.Transport)(nil)).Elem() + decoderTy = reflect.TypeOf((*types.TransactionDecoder)(nil)).Elem() +) diff --git a/rpc/client_test.go b/rpc/client_test.go deleted file mode 100644 index 302a5ab..0000000 --- a/rpc/client_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package rpc - -import ( - "bytes" - "context" - "io" - "math/big" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/defiweb/go-eth/types" -) - -func TestClient_Sign(t *testing.T) { - httpMock := newHTTPMock() - keyMock := &keyMock{} - keyMock.addressCallback = func() types.Address { - return types.MustAddressFromHex("0x1111111111111111111111111111111111111111") - } - keyMock.signMessageCallback = func(message []byte) (*types.Signature, error) { - return types.MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), nil - } - - client, _ := NewClient(WithTransport(httpMock), WithKeys(keyMock)) - - signature, err := client.Sign( - context.Background(), - types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), - []byte("All your base are belong to us"), - ) - require.NoError(t, err) - assert.Equal(t, types.MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), *signature) -} - -func TestClient_SignTransaction(t *testing.T) { - httpMock := newHTTPMock() - keyMock := &keyMock{} - keyMock.addressCallback = func() types.Address { - return types.MustAddressFromHex("0xb60e8dd61c5d32be8058bb8eb970870f07233155") - } - keyMock.signTransactionCallback = func(tx *types.Transaction) error { - tx.Signature = types.MustSignatureFromHexPtr("0x2222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333311") - return nil - } - - client, _ := NewClient(WithTransport(httpMock), WithKeys(keyMock)) - - from := types.MustAddressFromHex("0xb60e8dd61c5d32be8058bb8eb970870f07233155") - to := types.MustAddressFromHex("0xd46e8dd67c5d32be8058bb8eb970870f07244567") - gasLimit := uint64(30400) - chainID := uint64(1) - raw, tx, err := client.SignTransaction( - context.Background(), - &types.Transaction{ - ChainID: &chainID, - Call: types.Call{ - From: &from, - To: &to, - GasLimit: &gasLimit, - GasPrice: big.NewInt(10000000000000), - Value: big.NewInt(10000000000), - Input: hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"), - }, - }, - ) - - require.NoError(t, err) - assert.Equal(t, hexToBytes("0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333"), raw) - assert.Equal(t, &to, tx.To) - assert.Equal(t, uint64(30400), *tx.GasLimit) - assert.Equal(t, big.NewInt(10000000000000), tx.GasPrice) - assert.Equal(t, big.NewInt(10000000000), tx.Value) - assert.Equal(t, hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"), tx.Input) - assert.Equal(t, uint8(0x11), tx.Signature.Bytes()[64]) - assert.Equal(t, hexToBytes("0x2222222222222222222222222222222222222222222222222222222222222222"), tx.Signature.Bytes()[:32]) - assert.Equal(t, hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333"), tx.Signature.Bytes()[32:64]) -} - -func TestClient_SendTransaction(t *testing.T) { - httpMock := newHTTPMock() - keyMock := &keyMock{} - keyMock.addressCallback = func() types.Address { - return types.MustAddressFromHex("0xb60e8dd61c5d32be8058bb8eb970870f07233155") - } - keyMock.signTransactionCallback = func(tx *types.Transaction) error { - tx.Signature = types.MustSignatureFromHexPtr("0x2222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333311") - return nil - } - - client, _ := NewClient(WithTransport(httpMock), WithKeys(keyMock)) - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockSendRawTransactionResponse)), - } - - from := types.MustAddressFromHex("0xb60e8dd61c5d32be8058bb8eb970870f07233155") - to := types.MustAddressFromHex("0xd46e8dd67c5d32be8058bb8eb970870f07244567") - gasLimit := uint64(30400) - gasPrice := big.NewInt(10000000000000) - value := big.NewInt(10000000000) - input := hexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675") - chainID := uint64(1) - txHash, tx, err := client.SendTransaction( - context.Background(), - &types.Transaction{ - ChainID: &chainID, - Call: types.Call{ - From: &from, - To: &to, - GasLimit: &gasLimit, - GasPrice: gasPrice, - Value: value, - Input: input, - }, - }, - ) - require.NoError(t, err) - assert.JSONEq(t, mockSendRawTransactionRequest, readBody(httpMock.Request)) - assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), *txHash) - assert.Equal(t, &to, tx.To) - assert.Equal(t, gasLimit, *tx.GasLimit) - assert.Equal(t, gasPrice, tx.GasPrice) - assert.Equal(t, value, tx.Value) - assert.Equal(t, input, tx.Input) -} - -func TestClient_Call(t *testing.T) { - httpMock := newHTTPMock() - client, _ := NewClient( - WithTransport(httpMock), - WithDefaultAddress(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")), - ) - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockCallResponse)), - } - - to := types.MustAddressFromHex("0x2222222222222222222222222222222222222222") - gasLimit := uint64(30400) - _, _, err := client.Call( - context.Background(), - &types.Call{ - From: nil, - To: &to, - GasLimit: &gasLimit, - GasPrice: big.NewInt(10000000000000), - Value: big.NewInt(10000000000), - Input: hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333"), - }, - types.BlockNumberFromUint64(1), - ) - require.NoError(t, err) - assert.JSONEq(t, mockCallRequest, readBody(httpMock.Request)) -} - -func TestClient_EstimateGas(t *testing.T) { - httpMock := newHTTPMock() - client, _ := NewClient( - WithTransport(httpMock), - WithDefaultAddress(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")), - ) - - httpMock.ResponseMock = &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(mockEstimateGasResponse)), - } - - to := types.MustAddressFromHex("0x2222222222222222222222222222222222222222") - gasLimit := uint64(30400) - _, _, err := client.EstimateGas( - context.Background(), - &types.Call{ - From: nil, - To: &to, - GasLimit: &gasLimit, - GasPrice: big.NewInt(10000000000000), - Value: big.NewInt(10000000000), - Input: hexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333"), - }, - types.LatestBlockNumber, - ) - require.NoError(t, err) - assert.JSONEq(t, mockEstimateGasRequest, readBody(httpMock.Request)) -} diff --git a/rpc/hijack_address.go b/rpc/hijack_address.go new file mode 100644 index 0000000..06f1a97 --- /dev/null +++ b/rpc/hijack_address.go @@ -0,0 +1,57 @@ +package rpc + +import ( + "context" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// hijackAddress hijacks the "eth_sendTransaction", "eth_call" and +// "eth_estimateGas" methods sets the "from" field. +type hijackAddress struct { + address types.Address + replace bool +} + +// Call implements the [transport.Hijacker] interface. +func (h *hijackAddress) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 { + return next(ctx, t, result, method, args...) + } + var cd *types.CallData + switch method { + case "eth_sendTransaction": + tx, ok := args[0].(types.Transaction) + if !ok { + return next(ctx, t, result, method, args...) + } + cd = getCallData(tx) + case "eth_call", "eth_estimateGas": + call, ok := args[0].(types.Call) + if !ok { + return next(ctx, t, result, method, args...) + } + cd = getCallData(call) + default: + return next(ctx, t, result, method, args...) + } + if cd != nil && (h.replace || cd.From == nil) { + cd.From = &h.address + } + return next(ctx, t, result, method, args...) + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (h *hijackAddress) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (h *hijackAddress) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} diff --git a/rpc/hijack_address_test.go b/rpc/hijack_address_test.go new file mode 100644 index 0000000..d5a523a --- /dev/null +++ b/rpc/hijack_address_test.go @@ -0,0 +1,86 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +func TestHijackAddress(t *testing.T) { + tc := []struct { + name string + hijacker *hijackAddress + method string + args []any + request []string + response []string + }{ + { + name: "set address", + hijacker: &hijackAddress{replace: false, address: types.MustAddressFromHex("0x1111111111111111111111111111111111111111")}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + return types.NewTransactionAccessList() + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "replace address", + hijacker: &hijackAddress{replace: true, address: types.MustAddressFromHex("0x1111111111111111111111111111111111111111")}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetFrom(types.MustAddressFromHex("0x2222222222222222222222222222222222222222")) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + + hijacker := transport.NewHijacker(httpMock, tt.hijacker) + + err := hijacker.Call(context.Background(), nil, tt.method, tt.args...) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + require.NoError(t, err) + }) + } +} diff --git a/rpc/hijack_chainid.go b/rpc/hijack_chainid.go new file mode 100644 index 0000000..430faf1 --- /dev/null +++ b/rpc/hijack_chainid.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "context" + "fmt" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// hijackChainID hijacks the "eth_sendTransaction" method and sets the +// "chainID" field. +type hijackChainID struct { + chainID uint64 + replace bool +} + +// Call implements the [transport.Hijacker] interface. +func (h *hijackChainID) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + chainID := h.chainID + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 || method != "eth_sendTransaction" { + return next(ctx, t, result, method, args...) + } + tx, ok := args[0].(types.Transaction) + if !ok { + return next(ctx, t, result, method, args...) + } + td := getTransactionData(tx) + if td != nil && (h.replace || td.ChainID == nil) { + if chainID == 0 { + chainID, err = (&MethodsCommon{Transport: t}).ChainID(ctx) + if err != nil { + return &ErrHijackFailed{name: "chain ID", err: fmt.Errorf("failed to get chain ID: %w", err)} + } + } + td.ChainID = &chainID + } + return next(ctx, t, result, method, args...) + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (h *hijackChainID) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (h *hijackChainID) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} diff --git a/rpc/hijack_chainid_test.go b/rpc/hijack_chainid_test.go new file mode 100644 index 0000000..0205c80 --- /dev/null +++ b/rpc/hijack_chainid_test.go @@ -0,0 +1,105 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +func TestHijackChainID(t *testing.T) { + tc := []struct { + name string + hijacker *hijackChainID + method string + args []any + request []string + response []string + }{ + { + name: "set chainID", + hijacker: &hijackChainID{}, + method: "eth_sendTransaction", + args: []any{types.NewTransactionAccessList()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"chainId": "0x1"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id": 1,"result": "0x01"}`, + `{"jsonrpc":"2.0","id": 1,"result": "0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "do not replace chainID", + hijacker: &hijackChainID{replace: false}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetChainID(2) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_sendTransaction","params":[{"chainId": "0x2"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id": 1,"result": "0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "replace chainID", + hijacker: &hijackChainID{replace: true}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetChainID(2) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"chainId": "0x1"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id": 1,"result": "0x01"}`, + `{"jsonrpc":"2.0","id": 1,"result": "0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + + hijacker := transport.NewHijacker(httpMock, tt.hijacker) + + err := hijacker.Call(ctx, nil, tt.method, tt.args...) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + require.NoError(t, err) + }) + } +} diff --git a/rpc/hijack_error.go b/rpc/hijack_error.go new file mode 100644 index 0000000..dad792d --- /dev/null +++ b/rpc/hijack_error.go @@ -0,0 +1,13 @@ +package rpc + +import "fmt" + +// ErrHijackFailed is returned when a hijacker fails to process a request. +type ErrHijackFailed struct { + name string + err error +} + +func (e *ErrHijackFailed) Name() string { return e.name } +func (e *ErrHijackFailed) Error() string { return fmt.Sprintf("%s hijacker failed: %v", e.name, e.err) } +func (e *ErrHijackFailed) Unwrap() error { return e.err } diff --git a/rpc/hijack_gasfee.go b/rpc/hijack_gasfee.go new file mode 100644 index 0000000..9708970 --- /dev/null +++ b/rpc/hijack_gasfee.go @@ -0,0 +1,147 @@ +package rpc + +import ( + "context" + "fmt" + "math/big" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// hijackLegacyGasFee hijacks the "eth_sendTransaction" method and sets the +// "gasPrice" field using the estimate provided by the RPC node. +type hijackLegacyGasFee struct { + multiplier float64 + minGasPrice *big.Int + maxGasPrice *big.Int + replace bool + allowChangeType bool +} + +// Call implements the [transport.Hijacker] interface. +func (h *hijackLegacyGasFee) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 || method != "eth_sendTransaction" { + return next(ctx, t, result, method, args...) + } + tx, ok := args[0].(types.Transaction) + if !ok { + return next(ctx, t, result, method, args...) + } + if h.allowChangeType { + tx = convertTXToLegacyPrice(tx) + args[0] = tx + } + lpd := getLegacyPriceData(tx) + if lpd != nil && (h.replace || lpd.GasPrice == nil) { + gasPrice, err := (&MethodsCommon{Transport: t}).GasPrice(ctx) + if err != nil { + return &ErrHijackFailed{name: "legacy gas price", err: fmt.Errorf("failed to get gas price: %w", err)} + } + gasPrice, _ = new(big.Float).Mul(new(big.Float).SetInt(gasPrice), big.NewFloat(h.multiplier)).Int(nil) + if h.minGasPrice != nil && gasPrice.Cmp(h.minGasPrice) < 0 { + gasPrice = h.minGasPrice + } + if h.maxGasPrice != nil && gasPrice.Cmp(h.maxGasPrice) > 0 { + gasPrice = h.maxGasPrice + } + lpd.GasPrice = gasPrice + } + return next(ctx, t, result, method, args...) + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (h *hijackLegacyGasFee) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (h *hijackLegacyGasFee) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} + +// hijackDynamicGasFee hijacks the "eth_sendTransaction" method and sets the +// "maxFeePerGas" and "maxPriorityFeePerGas" fields using the estimates +// provided by the RPC node. +type hijackDynamicGasFee struct { + gasPriceMultiplier float64 + priorityFeePerGasMultiplier float64 + minGasPrice *big.Int + maxGasPrice *big.Int + minPriorityFeePerGas *big.Int + maxPriorityFeePerGas *big.Int + replace bool + allowChangeType bool +} + +// Call implements the [transport.Hijacker] interface. +func (h *hijackDynamicGasFee) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 || method != "eth_sendTransaction" { + return next(ctx, t, result, method, args...) + } + + // Obtain the dynamic fee data. + tx, ok := args[0].(types.Transaction) + if !ok { + return next(ctx, t, result, method, args...) + } + if h.allowChangeType { + tx = convertTXToDynamicFee(tx) + args[0] = tx + } + dfd := getDynamicFeeData(tx) + + // Set the dynamic fee fields if necessary. + if dfd != nil && (h.replace || dfd.MaxFeePerGas == nil || dfd.MaxPriorityFeePerGas == nil) { + // Fetch current gas prices from the RPC node. + maxFeePerGas, err := (&MethodsCommon{Transport: t}).GasPrice(ctx) + if err != nil { + return &ErrHijackFailed{name: "dynamic gas fee", err: fmt.Errorf("failed to get gas price: %w", err)} + } + priorityFeePerGas, err := (&MethodsCommon{Transport: t}).MaxPriorityFeePerGas(ctx) + if err != nil { + return &ErrHijackFailed{name: "dynamic gas fee", err: fmt.Errorf("failed to get priority fee per gas: %w", err)} + } + + // Apply multipliers and limits and set the fields. + maxFeePerGas, _ = new(big.Float).Mul(new(big.Float).SetInt(maxFeePerGas), big.NewFloat(h.gasPriceMultiplier)).Int(nil) + priorityFeePerGas, _ = new(big.Float).Mul(new(big.Float).SetInt(priorityFeePerGas), big.NewFloat(h.priorityFeePerGasMultiplier)).Int(nil) + if h.minGasPrice != nil && maxFeePerGas.Cmp(h.minGasPrice) < 0 { + maxFeePerGas = h.minGasPrice + } + if h.maxGasPrice != nil && maxFeePerGas.Cmp(h.maxGasPrice) > 0 { + maxFeePerGas = h.maxGasPrice + } + if h.minPriorityFeePerGas != nil && priorityFeePerGas.Cmp(h.minPriorityFeePerGas) < 0 { + priorityFeePerGas = h.minPriorityFeePerGas + } + if h.maxPriorityFeePerGas != nil && priorityFeePerGas.Cmp(h.maxPriorityFeePerGas) > 0 { + priorityFeePerGas = h.maxPriorityFeePerGas + } + if maxFeePerGas.Cmp(priorityFeePerGas) < 0 { + priorityFeePerGas = maxFeePerGas + } + dfd.MaxFeePerGas = maxFeePerGas + dfd.MaxPriorityFeePerGas = priorityFeePerGas + } + + return next(ctx, t, result, method, args...) + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (h *hijackDynamicGasFee) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (h *hijackDynamicGasFee) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} diff --git a/rpc/hijack_gasfee_test.go b/rpc/hijack_gasfee_test.go new file mode 100644 index 0000000..1c93f2a --- /dev/null +++ b/rpc/hijack_gasfee_test.go @@ -0,0 +1,126 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +func TestHijackLegacyGasFee(t *testing.T) { + tc := []struct { + name string + hijacker *hijackLegacyGasFee + method string + args []any + request []string + response []string + }{ + { + name: "set gas price", + hijacker: &hijackLegacyGasFee{multiplier: 1.0}, + method: "eth_sendTransaction", + args: []any{types.NewTransactionLegacy()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_gasPrice","params":[]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"gasPrice":"0x1000"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1000"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + + hijacker := transport.NewHijacker(httpMock, tt.hijacker) + + err := hijacker.Call(context.Background(), nil, tt.method, tt.args...) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + require.NoError(t, err) + }) + } +} + +func TestHijackDynamicGasFee(t *testing.T) { + tc := []struct { + name string + hijacker *hijackDynamicGasFee + method string + args []any + request []string + response []string + }{ + { + name: "set gas price", + hijacker: &hijackDynamicGasFee{gasPriceMultiplier: 1.0, priorityFeePerGasMultiplier: 1.0}, + method: "eth_sendTransaction", + args: []any{types.NewTransactionDynamicFee()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_gasPrice","params":[]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_maxPriorityFeePerGas","params":[]}`, + `{"jsonrpc":"2.0","id":3,"method":"eth_sendTransaction","params":[{"maxFeePerGas":"0x1000","maxPriorityFeePerGas":"0x100"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1000"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x100"}`, + `{"jsonrpc":"2.0","id":3,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + + hijacker := transport.NewHijacker(httpMock, tt.hijacker) + + err := hijacker.Call(context.Background(), nil, tt.method, tt.args...) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + require.NoError(t, err) + }) + } +} diff --git a/rpc/hijack_gaslimit.go b/rpc/hijack_gaslimit.go new file mode 100644 index 0000000..fc58172 --- /dev/null +++ b/rpc/hijack_gaslimit.go @@ -0,0 +1,61 @@ +package rpc + +import ( + "context" + "fmt" + "math/big" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// hijackGasLimit hijacks the "eth_sendTransaction" method and sets the +// "gasLimit" field using the estimate provided by the RPC node. +type hijackGasLimit struct { + multiplier float64 + minGas uint64 + maxGas uint64 + replace bool +} + +// Call implements the [transport.Hijacker] interface. +func (h *hijackGasLimit) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 || method != "eth_sendTransaction" { + return next(ctx, t, result, method, args...) + } + tx, ok := args[0].(types.Transaction) + if !ok { + return next(ctx, t, result, method, args...) + } + tc := tx.Call() + cd := getCallData(tx) + if tc != nil && cd != nil && (h.replace || cd.GasLimit == nil) { + gasLimit, err := (&MethodsCommon{Transport: t}).EstimateGas(ctx, tc, types.LatestBlockNumber) + if err != nil { + return &ErrHijackFailed{name: "gas limit", err: fmt.Errorf("failed to estimate gas: %w", err)} + } + gasLimit, _ = new(big.Float).Mul(new(big.Float).SetUint64(gasLimit), big.NewFloat(h.multiplier)).Uint64() + if h.minGas > 0 && gasLimit < h.minGas { + gasLimit = h.minGas + } + if h.maxGas > 0 && gasLimit > h.maxGas { + gasLimit = h.maxGas + } + cd.GasLimit = &gasLimit + } + return next(ctx, t, result, method, args...) + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (h *hijackGasLimit) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (h *hijackGasLimit) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} diff --git a/rpc/hijack_gaslimit_test.go b/rpc/hijack_gaslimit_test.go new file mode 100644 index 0000000..238acfb --- /dev/null +++ b/rpc/hijack_gaslimit_test.go @@ -0,0 +1,106 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +func TestHijackGasLimit(t *testing.T) { + tc := []struct { + name string + hijacker *hijackGasLimit + method string + args []any + request []string + response []string + }{ + { + name: "set gas limit", + hijacker: &hijackGasLimit{multiplier: 1.0}, + method: "eth_sendTransaction", + args: []any{types.NewTransactionAccessList()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_estimateGas","params":[{}, "latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"gas":"0x1000"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1000"}`, + `{"jsonrpc":"2.0","id":1,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "do not replace gas limit", + hijacker: &hijackGasLimit{multiplier: 1.0, replace: false}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetGasLimit(2) + tx.SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111", "gas": "0x2"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "replace gas limit", + hijacker: &hijackGasLimit{multiplier: 1.0, replace: true}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetGasLimit(2) + tx.SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_estimateGas","params":[{"from":"0x1111111111111111111111111111111111111111", "gas":"0x2"}, "latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111", "gas": "0x1"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x01"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tc := range tc { + t.Run(tc.name, func(t *testing.T) { + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tc.request) + require.NotEmpty(t, tc.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tc.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tc.request[0], string(body))) + + res := tc.response[0] + tc.request = tc.request[1:] + tc.response = tc.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + + hijacker := transport.NewHijacker(httpMock, tc.hijacker) + + err := hijacker.Call(context.Background(), nil, tc.method, tc.args...) + assert.Len(t, tc.request, 0) + assert.Len(t, tc.response, 0) + require.NoError(t, err) + }) + } +} diff --git a/rpc/hijack_nonce.go b/rpc/hijack_nonce.go new file mode 100644 index 0000000..93bd8fc --- /dev/null +++ b/rpc/hijack_nonce.go @@ -0,0 +1,55 @@ +package rpc + +import ( + "context" + "fmt" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// hijackNonce hijacks the "eth_sendTransaction" method and sets the "nonce" +// field using the "eth_getTransactionCount" RPC method. +type hijackNonce struct { + usePendingBlock bool + replace bool +} + +func (h *hijackNonce) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 || method != "eth_sendTransaction" { + return next(ctx, t, result, method, args...) + } + tx, ok := args[0].(types.Transaction) + if !ok { + return next(ctx, t, result, method, args...) + } + td := getTransactionData(tx) // to set the nonce + cd := getCallData(tx) // to get the "from" address + if td != nil && cd != nil && (h.replace || td.Nonce == nil) { + if cd.From == nil { + return &ErrHijackFailed{name: "nonce", err: fmt.Errorf("'from' field not set")} + } + block := types.LatestBlockNumber + if h.usePendingBlock { + block = types.PendingBlockNumber + } + nonce, err := (&MethodsCommon{Transport: t}).GetTransactionCount(ctx, *cd.From, block) + if err != nil { + return &ErrHijackFailed{name: "nonce", err: fmt.Errorf("failed to get transaction count: %w", err)} + } + td.Nonce = &nonce + } + return next(ctx, t, result, method, args...) + } + } +} + +func (h *hijackNonce) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +func (h *hijackNonce) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} diff --git a/rpc/hijack_nonce_test.go b/rpc/hijack_nonce_test.go new file mode 100644 index 0000000..0d35760 --- /dev/null +++ b/rpc/hijack_nonce_test.go @@ -0,0 +1,111 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +func TestHijackNonce(t *testing.T) { + tc := []struct { + name string + hijacker *hijackNonce + method string + args []any + request []string + response []string + }{ + { + name: "set nonce", + hijacker: &hijackNonce{}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount","params":["0x1111111111111111111111111111111111111111","latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111", "nonce": "0x1"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x01"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "do not replace nonce", + hijacker: &hijackNonce{replace: false}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetNonce(2) + tx.SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111", "nonce": "0x2"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + { + name: "replace nonce", + hijacker: &hijackNonce{replace: true}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionAccessList() + tx.SetNonce(2) + tx.SetFrom(types.MustAddressFromHex("0x1111111111111111111111111111111111111111")) + return tx + }()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount","params":["0x1111111111111111111111111111111111111111","latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"from": "0x1111111111111111111111111111111111111111", "nonce": "0x1"}]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x01"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + + hijacker := transport.NewHijacker(httpMock, tt.hijacker) + + err := hijacker.Call(ctx, nil, tt.method, tt.args...) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + require.NoError(t, err) + }) + } +} diff --git a/rpc/hijack_sign.go b/rpc/hijack_sign.go new file mode 100644 index 0000000..93f83c8 --- /dev/null +++ b/rpc/hijack_sign.go @@ -0,0 +1,150 @@ +package rpc + +import ( + "context" + "fmt" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" + "github.com/defiweb/go-eth/wallet" +) + +// hijackSign hijacks calls to methods that require account access +// and simulates their behavior using the provided keys. +type hijackSign struct { + keys []wallet.Key +} + +// Call implements the [transport.Hijacker] interface. +func (k *hijackSign) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) error { + switch { + case method == "eth_accounts": + accounts, ok := result.(*[]types.Address) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid result type: %T", args[0])} + } + k.hijackAccountsCall(accounts) + case method == "eth_sign" && len(args) == 2: + signature, ok := result.(*types.Signature) + if !ok { + return fmt.Errorf("invalid result type: %T", result) + } + account, ok := args[0].(types.Address) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid result type: %T", args[0])} + } + data, ok := args[1].([]byte) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid data type: %T", args[0])} + } + return k.hijackSignCall(ctx, signature, account, data) + case method == "eth_signTransaction" && len(args) == 1: + raw, ok := result.(*[]byte) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid result type: %T", result)} + } + tx, ok := args[0].(types.Transaction) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid transaction type: %T", args[0])} + } + return k.hijackSignTransactionCall(ctx, raw, tx) + case method == "eth_sendTransaction" && len(args) == 1: + hash, ok := result.(*types.Hash) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid result type: %T", args[0])} + } + tx, ok := args[0].(types.Transaction) + if !ok { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("invalid transaction type: %T", args[0])} + } + return k.hijackSendTransactionCall(ctx, t, next, hash, tx) + default: + return next(ctx, t, result, method, args...) + } + return nil + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (k *hijackSign) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (k *hijackSign) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} + +func (k *hijackSign) hijackAccountsCall(result *[]types.Address) { + *result = make([]types.Address, len(k.keys)) + for n, key := range k.keys { + (*result)[n] = key.Address() + } +} + +func (k *hijackSign) hijackSignCall(ctx context.Context, result *types.Signature, account types.Address, data []byte) error { + for _, key := range k.keys { + if key.Address() != account { + continue + } + signature, err := key.SignMessage(ctx, data) + if err != nil { + return err + } + *result = *signature + return nil + } + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("no key found for address %s", account)} +} + +func (k *hijackSign) hijackSignTransactionCall(ctx context.Context, result *[]byte, tx types.Transaction) error { + if len(k.keys) == 0 { + return fmt.Errorf("no keys found") + } + txcd := getCallData(tx) + if txcd.From == nil { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("'from' field not set")} + } + for _, key := range k.keys { + if key.Address() != *txcd.From { + continue + } + if err := key.SignTransaction(ctx, tx); err != nil { + return &ErrHijackFailed{name: "sign", err: err} + } + raw, err := tx.EncodeRLP() + if err != nil { + return &ErrHijackFailed{name: "sign", err: err} + } + *result = raw + return nil + } + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("no key found for address %s", *txcd.From)} +} + +func (k *hijackSign) hijackSendTransactionCall(ctx context.Context, t transport.Transport, next transport.CallFunc, result *types.Hash, tx types.Transaction) error { + if len(k.keys) == 0 { + return fmt.Errorf("no keys found") + } + txcd := getCallData(tx) + if txcd.From == nil { + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("'from' field not set")} + } + for _, key := range k.keys { + if key.Address() != *txcd.From { + continue + } + if err := key.SignTransaction(ctx, tx); err != nil { + return &ErrHijackFailed{name: "sign", err: err} + } + raw, err := tx.EncodeRLP() + if err != nil { + return &ErrHijackFailed{name: "sign", err: err} + } + return next(ctx, t, result, "eth_sendRawTransaction", types.Bytes(raw)) + } + return &ErrHijackFailed{name: "sign", err: fmt.Errorf("no key found for address %s", *txcd.From)} +} diff --git a/rpc/hijack_sign_test.go b/rpc/hijack_sign_test.go new file mode 100644 index 0000000..adc7250 --- /dev/null +++ b/rpc/hijack_sign_test.go @@ -0,0 +1,119 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" + "github.com/defiweb/go-eth/wallet" +) + +func TestHijackSign(t *testing.T) { + key1 := wallet.NewKeyFromBytes(hexutil.MustHexToBytes("0x01")) // 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf + key2 := wallet.NewKeyFromBytes(hexutil.MustHexToBytes("0x02")) // 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf + tc := []struct { + name string + sign *hijackSign + method string + args []any + wantResult any + request []string + response []string + }{ + { + name: "accounts", + sign: &hijackSign{}, + method: "eth_accounts", + args: []any{}, + wantResult: []types.Address{ + key1.Address(), + key2.Address(), + }, + request: []string{}, + response: []string{}, + }, + { + name: "sign", + sign: &hijackSign{}, + method: "eth_sign", + args: []any{ + key1.Address(), + []byte("hello"), + }, + wantResult: types.MustSignatureFromHex("0xe5ddc160e4c8f92de507c7db9b982d4f9b7197bfa421864aeadc586bc96b09ae0ba0c5b131650ae4994cff1839341d00f3735ef5abc62ac8fe2cf50f65208e2a1b"), + request: []string{}, + response: []string{}, + }, + { + name: "sign transaction", + sign: &hijackSign{}, + method: "eth_signTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionLegacy() + tx.SetFrom(types.MustAddressFromHex("0x7e5f4552091a69125d5dfcb7b8c2659029395bdf")) + return tx + }()}, + wantResult: hexutil.MustHexToBytes("0xf8498080808080801ba060ac8da6fc016ee487d06e93fec9768941a182638546857f6242cd86581d0174a02bea91a940d7647518956970f9ecd63847ed9aaa3a451ef7e20c89823f847082"), + request: []string{}, + response: []string{}, + }, + { + name: "send transaction", + sign: &hijackSign{}, + method: "eth_sendTransaction", + args: []any{func() types.Transaction { + tx := types.NewTransactionLegacy() + tx.SetFrom(types.MustAddressFromHex("0x7e5f4552091a69125d5dfcb7b8c2659029395bdf")) + return tx + }()}, + wantResult: types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf8498080808080801ba060ac8da6fc016ee487d06e93fec9768941a182638546857f6242cd86581d0174a02bea91a940d7647518956970f9ecd63847ed9aaa3a451ef7e20c89823f847082"]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + tt.sign.keys = []wallet.Key{key1, key2} + hijacker := transport.NewHijacker(httpMock, tt.sign) + + result := reflect.New(reflect.TypeOf(tt.wantResult)) + err := hijacker.Call(ctx, result.Interface(), tt.method, tt.args...) + assert.Equal(t, tt.wantResult, result.Elem().Interface()) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + require.NoError(t, err) + }) + } +} diff --git a/rpc/hijack_simulate.go b/rpc/hijack_simulate.go new file mode 100644 index 0000000..bf4f117 --- /dev/null +++ b/rpc/hijack_simulate.go @@ -0,0 +1,89 @@ +package rpc + +import ( + "context" + "fmt" + + "github.com/defiweb/go-eth/crypto/txsign" + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// hijackSimulate hijacks "eth_send*Transaction" methods and simulates the +// transaction execution before sending it. +type hijackSimulate struct { + decoder types.RLPTransactionDecoder +} + +// Call implements the [transport.Hijacker] interface. +func (h *hijackSimulate) Call() func(next transport.CallFunc) transport.CallFunc { + return func(next transport.CallFunc) transport.CallFunc { + return func(ctx context.Context, t transport.Transport, result any, method string, args ...any) (err error) { + if len(args) == 0 { + return next(ctx, t, result, method, args...) + } + switch method { + case "eth_sendTransaction": + tx, ok := args[0].(types.Transaction) + if !ok { + return &ErrHijackFailed{name: "simulate", err: fmt.Errorf("no transaction found")} + } + if err := h.simulate(ctx, t, tx); err != nil { + return &ErrHijackFailed{name: "simulate", err: err} + } + case "eth_sendRawTransaction", "eth_sendPrivateTransaction": + raw, ok := args[0].(types.Bytes) + if !ok { + return &ErrHijackFailed{name: "simulate", err: fmt.Errorf("no raw transaction found")} + } + tx, err := h.decoder.DecodeRLP(raw) + if err != nil { + return &ErrHijackFailed{name: "simulate", err: fmt.Errorf("failed to decode transaction: %w", err)} + } + if err := h.simulate(ctx, t, tx); err != nil { + return &ErrHijackFailed{name: "simulate", err: err} + } + } + return next(ctx, t, result, method, args...) + } + } +} + +// Subscribe implements the [transport.Hijacker] interface. +func (h *hijackSimulate) Subscribe() func(next transport.SubscribeFunc) transport.SubscribeFunc { + return nil +} + +// Unsubscribe implements the [transport.Hijacker] interface. +func (h *hijackSimulate) Unsubscribe() func(next transport.UnsubscribeFunc) transport.UnsubscribeFunc { + return nil +} + +func (h *hijackSimulate) simulate(ctx context.Context, t transport.Transport, tx types.Transaction) error { + // If the transaction has not a corrseponding call, we cannot simulate it. + // This can happen for custom transactions types. + call := tx.Call() + if call == nil { + return nil + } + + // Recover the transaction sender if it is not present. + // This can happen if the transaction is encoded using RLP, as this format + // contains only the signature and not the sender address. + if td := tx.GetTransactionData(); td.Signature != nil && call.GetCallData().From == nil { + from, err := txsign.Recover(tx) + if err != nil { + return fmt.Errorf("unable to recover transaction sender: %w", err) + } + call.GetCallData().From = from + if cd := getCallData(tx); cd != nil { + cd.From = from + } + } + + // Simulate the transaction. + if _, err := (&MethodsCommon{Transport: t}).Call(ctx, call, types.LatestBlockNumber); err != nil { + return err + } + return nil +} diff --git a/rpc/hijack_simulate_test.go b/rpc/hijack_simulate_test.go new file mode 100644 index 0000000..804f8c5 --- /dev/null +++ b/rpc/hijack_simulate_test.go @@ -0,0 +1,136 @@ +package rpc + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +func TestHijackSimulate(t *testing.T) { + tc := []struct { + name string + method string + args []any + request []string + response []string + wantResult any + wantErr string + }{ + { + name: "send valid transaction", + method: "eth_sendTransaction", + args: []any{ + func() types.Transaction { + tx := types.NewTransactionLegacy() + _, _ = tx.DecodeRLP(hexutil.MustHexToBytes("0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83")) + return tx + }(), + }, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"from":"0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f","to":"0x3535353535353535353535353535353535353535","gas":"0x5208","gasPrice":"0x4a817c800","value":"0xde0b6b3a7640000"},"latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{"chainId":"0x1","from":"0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f","to":"0x3535353535353535353535353535353535353535","gas":"0x5208","gasPrice":"0x4a817c800","nonce":"0x9","value":"0xde0b6b3a7640000","v":"0x25","r":"0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276","s":"0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"}]}`, + }, + response: []string{ + `{"jsonrpc": "2.0","id": 1,"result": "0x01"}`, + `{"jsonrpc": "2.0","id": 1,"result": "0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + wantResult: "0x1111111111111111111111111111111111111111111111111111111111111111", + }, + { + name: "send valid raw transaction", + method: "eth_sendRawTransaction", + args: []any{types.MustBytesFromHex("0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83")}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"from":"0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f","to":"0x3535353535353535353535353535353535353535","gas":"0x5208","gasPrice":"0x4a817c800","value":"0xde0b6b3a7640000"},"latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendRawTransaction","params":["0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x01"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + wantResult: "0x1111111111111111111111111111111111111111111111111111111111111111", + }, + { + name: "send valid private transaction", + method: "eth_sendPrivateTransaction", + args: []any{types.MustBytesFromHex("0xc9808080808080808080")}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{},"latest"]}`, + `{"jsonrpc":"2.0","id":2,"method":"eth_sendPrivateTransaction","params":["0xc9808080808080808080"]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"result":"0x01"}`, + `{"jsonrpc":"2.0","id":2,"result":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + }, + wantResult: "0x1111111111111111111111111111111111111111111111111111111111111111", + }, + { + name: "send invalid raw transaction", + method: "eth_sendTransaction", + args: []any{types.NewTransactionLegacy()}, + request: []string{ + `{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{},"latest"]}`, + }, + response: []string{ + `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"revert","data":"0x08c379a0"}}`, + }, + wantErr: "revert", + }, + { + name: "invalid raw data", + method: "eth_sendRawTransaction", + args: []any{types.MustBytesFromHex("c9")}, + request: []string{}, + response: []string{}, + wantErr: "failed to decode transaction", + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + httpMock := newHTTPMock() + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + require.NotEmpty(t, tt.request) + require.NotEmpty(t, tt.response) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.JSONEq(t, tt.request[0], string(body), fmt.Sprintf("expected: %s, got: %s", tt.request[0], string(body))) + + res := tt.response[0] + tt.request = tt.request[1:] + tt.response = tt.response[1:] + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(res)), + }, nil + } + hijacker := transport.NewHijacker(httpMock, &hijackSimulate{ + decoder: types.DefaultTransactionDecoder, + }) + + var result any + err := hijacker.Call(ctx, &result, tt.method, tt.args...) + assert.Len(t, tt.request, 0) + assert.Len(t, tt.response, 0) + + if tt.wantErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantResult, result) + }) + } +} diff --git a/rpc/methods_client.go b/rpc/methods_client.go new file mode 100644 index 0000000..774e40a --- /dev/null +++ b/rpc/methods_client.go @@ -0,0 +1,71 @@ +package rpc + +import ( + "context" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// MethodsClient is a collection of RPC methods that provide information about +// the node. +// +// Note: Some JSON-RPC APIs do not support these methods. +type MethodsClient struct { + Transport transport.Transport +} + +// ClientVersion performs web3_clientVersion RPC call. +// +// It returns the current client version. +func (c *MethodsClient) ClientVersion(ctx context.Context) (string, error) { + var res string + if err := c.Transport.Call(ctx, &res, "web3_clientVersion"); err != nil { + return "", err + } + return res, nil +} + +// NetworkID performs net_version RPC call. +// +// It returns the current network ID. +func (c *MethodsClient) NetworkID(ctx context.Context) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "net_version"); err != nil { + return 0, err + } + return res.Big().Uint64(), nil +} + +// Listening performs net_listening RPC call. +// +// It returns true if the client is actively listening for network. +func (c *MethodsClient) Listening(ctx context.Context) (bool, error) { + var res bool + if err := c.Transport.Call(ctx, &res, "net_listening"); err != nil { + return false, err + } + return res, nil +} + +// PeerCount performs net_peerCount RPC call. +// +// It returns the number of connected peers. +func (c *MethodsClient) PeerCount(ctx context.Context) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "net_peerCount"); err != nil { + return 0, err + } + return res.Big().Uint64(), nil +} + +// Syncing performs eth_syncing RPC call. +// +// It returns an object with data about the sync status or false. +func (c *MethodsClient) Syncing(ctx context.Context) (*types.SyncStatus, error) { + var res types.SyncStatus + if err := c.Transport.Call(ctx, &res, "eth_syncing"); err != nil { + return nil, err + } + return &res, nil +} diff --git a/rpc/methods_client_test.go b/rpc/methods_client_test.go new file mode 100644 index 0000000..49aebf6 --- /dev/null +++ b/rpc/methods_client_test.go @@ -0,0 +1,193 @@ +package rpc + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const mockClientVersionRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "web3_clientVersion", + "params": [] + } +` + +const mockClientVersionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "Geth/v1.9.25-unstable-3f0b5e4e-20201014/linux-amd64/go1.15.2" + } +` + +func TestBaseClient_ClientVersion(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsClient{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockClientVersionRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockClientVersionResponse)), + }, nil + } + + clientVersion, err := client.ClientVersion(context.Background()) + + require.NoError(t, err) + assert.Equal(t, "Geth/v1.9.25-unstable-3f0b5e4e-20201014/linux-amd64/go1.15.2", clientVersion) +} + +const mockNetworkIDRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "net_version", + "params": [] + } +` + +const mockNetworkIDResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_NetworkID(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsClient{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockNetworkIDRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockNetworkIDResponse)), + }, nil + } + + networkID, err := client.NetworkID(context.Background()) + + require.NoError(t, err) + assert.Equal(t, uint64(1), networkID) +} + +const mockListeningRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "net_listening", + "params": [] + } +` + +const mockListeningResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": true + } +` + +func TestBaseClient_Listening(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsClient{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockListeningRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockListeningResponse)), + }, nil + } + + listening, err := client.Listening(context.Background()) + + require.NoError(t, err) + assert.True(t, listening) +} + +const mockPeerCountRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "net_peerCount", + "params": [] + } +` + +const mockPeerCountResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_PeerCount(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsClient{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockPeerCountRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockPeerCountResponse)), + }, nil + } + + peerCount, err := client.PeerCount(context.Background()) + + require.NoError(t, err) + assert.Equal(t, uint64(1), peerCount) +} + +const mockSyncingRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_syncing", + "params": [] + } +` + +const mockSyncingResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "startingBlock": "0x384", + "currentBlock": "0x386", + "highestBlock": "0x454" + } + } +` + +func TestBaseClient_Syncing(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsClient{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockSyncingRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockSyncingResponse)), + }, nil + } + + syncStatus, err := client.Syncing(context.Background()) + + require.NoError(t, err) + assert.Equal(t, uint64(0x384), syncStatus.StartingBlock.Big().Uint64()) + assert.Equal(t, uint64(0x386), syncStatus.CurrentBlock.Big().Uint64()) + assert.Equal(t, uint64(0x454), syncStatus.HighestBlock.Big().Uint64()) +} diff --git a/rpc/methods_common.go b/rpc/methods_common.go new file mode 100644 index 0000000..0246787 --- /dev/null +++ b/rpc/methods_common.go @@ -0,0 +1,391 @@ +package rpc + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// MethodsCommon is a collection of methods that are commonly supported by +// Ethereum JSON-RPC APIs. +type MethodsCommon struct { + Transport transport.Transport + Decoder types.TransactionDecoder +} + +// +// Account methods: +// + +// GetBalance performs eth_getBalance RPC call. +// +// It returns the balance of the account of given address in wei. +func (c *MethodsCommon) GetBalance(ctx context.Context, address types.Address, block types.BlockNumber) (*big.Int, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_getBalance", address, block); err != nil { + return nil, err + } + return res.Big(), nil +} + +// GetCode performs eth_getCode RPC call. +// +// It returns the contract code at the given address. +func (c *MethodsCommon) GetCode(ctx context.Context, account types.Address, block types.BlockNumber) ([]byte, error) { + var res types.Bytes + if err := c.Transport.Call(ctx, &res, "eth_getCode", account, block); err != nil { + return nil, err + } + return res.Bytes(), nil +} + +// GetStorageAt performs eth_getStorageAt RPC call. +// +// It returns the value of key in the contract storage at the given +// address. +func (c *MethodsCommon) GetStorageAt(ctx context.Context, account types.Address, key types.Hash, block types.BlockNumber) (*types.Hash, error) { + var res types.Hash + if err := c.Transport.Call(ctx, &res, "eth_getStorageAt", account, key, block); err != nil { + return nil, err + } + return &res, nil +} + +// GetTransactionCount performs eth_getTransactionCount RPC call. +// +// It returns the number of transactions sent from the given address. +func (c *MethodsCommon) GetTransactionCount(ctx context.Context, account types.Address, block types.BlockNumber) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_getTransactionCount", account, block); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, errors.New("transaction count is too big") + } + return res.Big().Uint64(), nil +} + +// +// Block methods: +// + +// BlockByHash performs eth_getBlockByHash RPC call. +// +// It returns information about a block by hash. +func (c *MethodsCommon) BlockByHash(ctx context.Context, hash types.Hash, full bool) (*types.Block, error) { + var res types.Block + if err := c.Transport.Call(ctx, &res, "eth_getBlockByHash", hash, full); err != nil { + return nil, err + } + return &res, nil +} + +// BlockByNumber performs eth_getBlockByNumber RPC call. +// +// It returns the block with the given number. +func (c *MethodsCommon) BlockByNumber(ctx context.Context, number types.BlockNumber, full bool) (*types.Block, error) { + var res types.Block + if err := c.Transport.Call(ctx, &res, "eth_getBlockByNumber", number, full); err != nil { + return nil, err + } + return &res, nil +} + +// GetBlockTransactionCountByHash performs eth_getBlockTransactionCountByHash RPC call. +// +// It returns the number of transactions in the block with the given hash. +func (c *MethodsCommon) GetBlockTransactionCountByHash(ctx context.Context, hash types.Hash) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_getBlockTransactionCountByHash", hash); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, errors.New("transaction count is too big") + } + return res.Big().Uint64(), nil +} + +// GetBlockTransactionCountByNumber implements the RPC interface. +func (c *MethodsCommon) GetBlockTransactionCountByNumber(ctx context.Context, number types.BlockNumber) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_getBlockTransactionCountByNumber", number); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, errors.New("transaction count is too big") + } + return res.Big().Uint64(), nil +} + +// GetUncleByBlockHashAndIndex performs eth_getUncleByBlockNumberAndIndex RPC call. +// +// It returns information about an uncle of a block by number and uncle index position. +func (c *MethodsCommon) GetUncleByBlockHashAndIndex(ctx context.Context, hash types.Hash, index uint64) (*types.Block, error) { + var res types.Block + if err := c.Transport.Call(ctx, &res, "eth_getUncleByBlockHashAndIndex", hash, types.NumberFromUint64(index)); err != nil { + return nil, err + } + return &res, nil +} + +// GetUncleByBlockNumberAndIndex performs eth_getUncleByBlockNumberAndIndex RPC call. +// +// It returns information about an uncle of a block by hash and uncle index position. +func (c *MethodsCommon) GetUncleByBlockNumberAndIndex(ctx context.Context, number types.BlockNumber, index uint64) (*types.Block, error) { + var res types.Block + if err := c.Transport.Call(ctx, &res, "eth_getUncleByBlockNumberAndIndex", number, types.NumberFromUint64(index)); err != nil { + return nil, err + } + return &res, nil +} + +// GetUncleCountByBlockHash performs eth_getUncleCountByBlockHash RPC call. +// +// It returns the number of uncles in the block with the given hash. +func (c *MethodsCommon) GetUncleCountByBlockHash(ctx context.Context, hash types.Hash) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_getUncleCountByBlockHash", hash); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, errors.New("uncle count is too big") + } + return res.Big().Uint64(), nil +} + +// GetUncleCountByBlockNumber performs eth_getUncleCountByBlockNumber RPC call. +// +// It returns the number of uncles in the block with the given block number. +func (c *MethodsCommon) GetUncleCountByBlockNumber(ctx context.Context, number types.BlockNumber) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_getUncleCountByBlockNumber", number); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, errors.New("uncle count is too big") + } + return res.Big().Uint64(), nil +} + +// SubscribeNewHeads performs eth_subscribe RPC call with "newHeads" +// subscription type. +// +// It creates a subscription that will send new block headers. +// +// Subscription channel will be closed when the context is canceled. +func (c *MethodsCommon) SubscribeNewHeads(ctx context.Context) (<-chan types.Block, error) { + return subscribe[types.Block](ctx, c.Transport, "newHeads") +} + +// +// Transaction methods: +// + +// Call performs eth_call RPC call. +// +// It executes a new message call immediately without creating a +// transaction on the blockchain. +// +// If call also implements types.Transaction, then a Call method of the +// transaction will be used to create a call. +func (c *MethodsCommon) Call(ctx context.Context, call types.Call, block types.BlockNumber) ([]byte, error) { + if call == nil { + return nil, errors.New("rpc client: call is nil") + } + if tx, ok := call.(types.Transaction); ok { + call = tx.Call() + } + var res types.Bytes + if err := c.Transport.Call(ctx, &res, "eth_call", call, block); err != nil { + return nil, err + } + return res, nil +} + +// EstimateGas performs eth_estimateGas RPC call. +// +// It estimates the gas necessary to execute a specific transaction. +// +// If call also implements types.Transaction, then a Call method of the +// transaction will be used to create a call. +func (c *MethodsCommon) EstimateGas(ctx context.Context, call types.Call, block types.BlockNumber) (uint64, error) { + if call == nil { + return 0, errors.New("rpc client: call is nil") + } + if tx, ok := call.(types.Transaction); ok { + call = tx.Call() + } + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_estimateGas", call, block); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, errors.New("gas estimate is too big") + } + return res.Big().Uint64(), nil +} + +// SendRawTransaction performs eth_sendRawTransaction RPC call. +// +// It sends an encoded transaction to the network. +func (c *MethodsCommon) SendRawTransaction(ctx context.Context, data []byte) (*types.Hash, error) { + var res types.Hash + if err := c.Transport.Call(ctx, &res, "eth_sendRawTransaction", types.Bytes(data)); err != nil { + return nil, err + } + return &res, nil +} + +// GetTransactionByHash performs eth_getTransactionByHash RPC call. +// +// It returns the information about a transaction requested by transaction. +func (c *MethodsCommon) GetTransactionByHash(ctx context.Context, hash types.Hash) (*types.TransactionOnChain, error) { + res := types.TransactionOnChain{Decoder: c.Decoder} + if err := c.Transport.Call(ctx, &res, "eth_getTransactionByHash", hash); err != nil { + return nil, err + } + return &res, nil +} + +// GetTransactionByBlockHashAndIndex performs eth_getTransactionByBlockHashAndIndex RPC call. +// +// It returns the information about a transaction requested by transaction. +func (c *MethodsCommon) GetTransactionByBlockHashAndIndex(ctx context.Context, hash types.Hash, index uint64) (*types.TransactionOnChain, error) { + res := types.TransactionOnChain{Decoder: c.Decoder} + if err := c.Transport.Call(ctx, &res, "eth_getTransactionByBlockHashAndIndex", hash, types.NumberFromUint64(index)); err != nil { + return nil, err + } + return &res, nil +} + +// GetTransactionByBlockNumberAndIndex performs eth_getTransactionByBlockNumberAndIndex RPC call. +// +// It returns the information about a transaction requested by transaction. +func (c *MethodsCommon) GetTransactionByBlockNumberAndIndex(ctx context.Context, number types.BlockNumber, index uint64) (*types.TransactionOnChain, error) { + res := types.TransactionOnChain{Decoder: c.Decoder} + if err := c.Transport.Call(ctx, &res, "eth_getTransactionByBlockNumberAndIndex", number, types.NumberFromUint64(index)); err != nil { + return nil, err + } + return &res, nil +} + +// GetTransactionReceipt performs eth_getTransactionReceipt RPC call. +// +// It returns the receipt of a transaction by transaction hash. +func (c *MethodsCommon) GetTransactionReceipt(ctx context.Context, hash types.Hash) (*types.TransactionReceipt, error) { + var res types.TransactionReceipt + if err := c.Transport.Call(ctx, &res, "eth_getTransactionReceipt", hash); err != nil { + return nil, err + } + return &res, nil +} + +// GetBlockReceipts performs eth_getBlockReceipts RPC call. +// +// It returns all transaction receipts for a given block hash or number. +func (c *MethodsCommon) GetBlockReceipts(ctx context.Context, block types.BlockNumber) ([]*types.TransactionReceipt, error) { + var res []*types.TransactionReceipt + if err := c.Transport.Call(ctx, &res, "eth_getBlockReceipts", block); err != nil { + return nil, err + } + return res, nil +} + +// SubscribeNewPendingTransactions performs eth_subscribe RPC call with +// "newPendingTransactions" subscription type. +// +// It creates a subscription that will send new pending transactions. +// +// Subscription channel will be closed when the context is canceled. +func (c *MethodsCommon) SubscribeNewPendingTransactions(ctx context.Context) (<-chan types.Hash, error) { + return subscribe[types.Hash](ctx, c.Transport, "newPendingTransactions") +} + +// +// Logs methods: +// + +// GetLogs performs eth_getLogs RPC call. +// +// It returns logs that match the given query. +func (c *MethodsCommon) GetLogs(ctx context.Context, query *types.FilterLogsQuery) ([]types.Log, error) { + var res []types.Log + if err := c.Transport.Call(ctx, &res, "eth_getLogs", query); err != nil { + return nil, err + } + return res, nil +} + +// SubscribeLogs performs eth_subscribe RPC call with "logs" subscription +// type. +// +// It creates a subscription that will send logs that match the given query. +// +// Subscription channel will be closed when the context is canceled. +func (c *MethodsCommon) SubscribeLogs(ctx context.Context, query *types.FilterLogsQuery) (<-chan types.Log, error) { + return subscribe[types.Log](ctx, c.Transport, "logs", query) +} + +// Network status methods: + +// ChainID performs eth_chainId RPC call. +// +// It returns the current chain ID. +func (c *MethodsCommon) ChainID(ctx context.Context) (uint64, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_chainId"); err != nil { + return 0, err + } + if !res.Big().IsUint64() { + return 0, fmt.Errorf("chain id is too big") + } + return res.Big().Uint64(), nil +} + +// BlockNumber performs eth_blockNumber RPC call. +// +// It returns the current block number. +func (c *MethodsCommon) BlockNumber(ctx context.Context) (*big.Int, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_blockNumber"); err != nil { + return nil, err + } + return res.Big(), nil +} + +// GasPrice performs eth_gasPrice RPC call. +// +// It returns the current price per gas in wei. +func (c *MethodsCommon) GasPrice(ctx context.Context) (*big.Int, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_gasPrice"); err != nil { + return nil, err + } + return res.Big(), nil +} + +// MaxPriorityFeePerGas performs eth_maxPriorityFeePerGas RPC call. +// +// It returns the estimated maximum priority fee per gas. +func (c *MethodsCommon) MaxPriorityFeePerGas(ctx context.Context) (*big.Int, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_maxPriorityFeePerGas"); err != nil { + return nil, err + } + return res.Big(), nil +} + +// BlobBaseFee performs eth_blobBaseFee RPC call. +// +// It returns the expected base fee for blobs in the next block. +func (c *MethodsCommon) BlobBaseFee(ctx context.Context) (*big.Int, error) { + var res types.Number + if err := c.Transport.Call(ctx, &res, "eth_blobBaseFee"); err != nil { + return nil, err + } + return res.Big(), nil +} diff --git a/rpc/methods_common_test.go b/rpc/methods_common_test.go new file mode 100644 index 0000000..9543cc0 --- /dev/null +++ b/rpc/methods_common_test.go @@ -0,0 +1,1411 @@ +package rpc + +import ( + "bytes" + "context" + "encoding/json" + "io" + "math/big" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/types" +) + +const mockBlockResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "number": "0x11", + "hash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "parentHash": "0x3333333333333333333333333333333333333333333333333333333333333333", + "nonce": "0x4444444444444444", + "sha3Uncles": "0x5555555555555555555555555555555555555555555555555555555555555555", + "logsBloom": "0x66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666", + "transactionsRoot": "0x7777777777777777777777777777777777777777777777777777777777777777", + "stateRoot": "0x8888888888888888888888888888888888888888888888888888888888888888", + "receiptsRoot": "0x9999999999999999999999999999999999999999999999999999999999999999", + "miner": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "difficulty": "0xbbbbbb", + "totalDifficulty": "0xcccccc", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "size": "0xdddddd", + "gasLimit": "0xeeeeee", + "gasUsed": "0xffffff", + "timestamp": "0x54e34e8e", + "transactions": [ + { + "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "nonce": "0x22", + "blockHash": "0x3333333333333333333333333333333333333333333333333333333333333333", + "blockNumber": "0x4444", + "transactionIndex": "0x01", + "from": "0x5555555555555555555555555555555555555555", + "to": "0x6666666666666666666666666666666666666666", + "value": "0x2540be400", + "gas": "0x76c0", + "gasPrice": "0x9184e72a000", + "input": "0x777777777777" + } + ], + "uncles": [ + "0x8888888888888888888888888888888888888888888888888888888888888888" + ] + } + } +` + +const mockOnChainTransactionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockNumber": "0x22", + "from": "0x3333333333333333333333333333333333333333", + "gas": "0x76c0", + "gasPrice": "0x9184e72a000", + "hash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "input": "0x555555555555", + "nonce": "0x66", + "to": "0x7777777777777777777777777777777777777777", + "transactionIndex": "0x0", + "value": "0x2540be400", + "v": "0x88", + "r": "0x9999999999999999999999999999999999999999999999999999999999999999", + "s": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } +` + +const mockGetBalanceRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getBalance", + "params": [ + "0x1111111111111111111111111111111111111111", + "latest" + ] + } +` + +const mockGetBalanceResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x0234c8a3397aab58" + } +` + +func TestBaseClient_GetBalance(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetBalanceRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetBalanceResponse)), + }, nil + } + + balance, err := client.GetBalance( + context.Background(), + types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), + types.LatestBlockNumber, + ) + + require.NoError(t, err) + assert.Equal(t, big.NewInt(158972490234375000), balance) +} + +const mockGetCodeRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getCode", + "params": [ + "0x1111111111111111111111111111111111111111", + "0x2" + ] + } +` + +const mockGetCodeResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x3333333333333333333333333333333333333333333333333333333333333333" + } +` + +func TestBaseClient_GetCode(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetCodeRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetCodeResponse)), + }, nil + } + + code, err := client.GetCode( + context.Background(), + types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), + types.MustBlockNumberFromHex("0x2"), + ) + + require.NoError(t, err) + assert.Equal(t, "0x3333333333333333333333333333333333333333333333333333333333333333", hexutil.BytesToHex(code)) +} + +const mockGetStorageAtRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getStorageAt", + "params": [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222222222222222222222222222", + "0x1" + ] + } +` + +const mockGetStorageAtResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x3333333333333333333333333333333333333333333333333333333333333333" + } +` + +func TestBaseClient_GetStorageAt(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetStorageAtRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetStorageAtResponse)), + }, nil + } + + storage, err := client.GetStorageAt( + context.Background(), + types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), + types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), + types.MustBlockNumberFromHex("0x1"), + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x3333333333333333333333333333333333333333333333333333333333333333", types.PadNone), *storage) +} + +const mockGetTransactionCountRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getTransactionCount", + "params": [ + "0x1111111111111111111111111111111111111111", + "0x1" + ] + } +` + +const mockGetTransactionCountResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_GetTransactionCount(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetTransactionCountRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionCountResponse)), + }, nil + } + + transactionCount, err := client.GetTransactionCount( + context.Background(), + types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), + types.MustBlockNumberFromHex("0x1"), + ) + + require.NoError(t, err) + assert.Equal(t, uint64(1), transactionCount) +} + +const mockBlockByHashRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getBlockByHash", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111", + true + ] + } +` + +func TestBaseClient_BlockByHash(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockBlockByHashRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockBlockResponse)), + }, nil + } + + block, err := client.BlockByHash( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + true, + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) +} + +const mockBlockByNumberRequest = ` + { + "method": "eth_getBlockByNumber", + "params": [ + "0x1", + true + ], + "id": 1, + "jsonrpc": "2.0" + } +` + +func TestBaseClient_BlockByNumber(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockBlockByNumberRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockBlockResponse)), + }, nil + } + + block, err := client.BlockByNumber( + context.Background(), + types.MustBlockNumberFromHex("0x1"), + true, + ) + + require.NoError(t, err) + assert.Equal(t, big.NewInt(0x11), block.Number) + assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) + assert.Equal(t, types.MustHashFromHex("0x3333333333333333333333333333333333333333333333333333333333333333", types.PadNone), block.ParentHash) + assert.Equal(t, hexutil.MustHexToBigInt("0x4444444444444444"), block.Nonce) + assert.Equal(t, types.MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", types.PadNone), block.Sha3Uncles) + assert.Equal(t, hexutil.MustHexToBytes("0x66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666"), block.LogsBloom) + assert.Equal(t, types.MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", types.PadNone), block.TransactionsRoot) + assert.Equal(t, types.MustHashFromHex("0x8888888888888888888888888888888888888888888888888888888888888888", types.PadNone), block.StateRoot) + assert.Equal(t, types.MustHashFromHex("0x9999999999999999999999999999999999999999999999999999999999999999", types.PadNone), block.ReceiptsRoot) + assert.Equal(t, types.MustAddressFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), block.Miner) + assert.Equal(t, hexutil.MustHexToBigInt("0xbbbbbb"), block.Difficulty) + assert.Equal(t, hexutil.MustHexToBigInt("0xcccccc"), block.TotalDifficulty) + assert.Equal(t, hexutil.MustHexToBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), block.ExtraData) + assert.Equal(t, hexutil.MustHexToBigInt("0xdddddd").Uint64(), block.Size) + assert.Equal(t, hexutil.MustHexToBigInt("0xeeeeee").Uint64(), block.GasLimit) + assert.Equal(t, hexutil.MustHexToBigInt("0xffffff").Uint64(), block.GasUsed) + assert.Equal(t, int64(1424182926), block.Timestamp.Unix()) + require.Len(t, block.Transactions, 1) + require.Len(t, block.Uncles, 1) + + tx := block.Transactions[0].Transaction.(*types.TransactionLegacy) + assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), block.Transactions[0].Hash) + assert.Equal(t, uint64(0x22), *tx.Nonce) + assert.Equal(t, types.MustAddressFromHexPtr("0x5555555555555555555555555555555555555555"), tx.From) + assert.Equal(t, types.MustAddressFromHexPtr("0x6666666666666666666666666666666666666666"), tx.To) + assert.Equal(t, big.NewInt(10000000000), tx.Value) + assert.Equal(t, uint64(30400), *tx.GasLimit) + assert.Equal(t, big.NewInt(10000000000000), tx.GasPrice) + assert.Equal(t, hexutil.MustHexToBytes("0x777777777777"), tx.Input) + assert.Equal(t, types.MustHashFromHex("0x8888888888888888888888888888888888888888888888888888888888888888", types.PadNone), block.Uncles[0]) +} + +const mockGetBlockTransactionCountByHashRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111" + ] + } +` + +const mockGetBlockTransactionCountByHashResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_GetBlockTransactionCountByHash(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetBlockTransactionCountByHashRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetBlockTransactionCountByHashResponse)), + }, nil + } + + transactionCount, err := client.GetBlockTransactionCountByHash( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + ) + + require.NoError(t, err) + assert.Equal(t, uint64(1), transactionCount) +} + +const mockGetBlockTransactionCountByNumberRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "0x1" + ] + } +` + +const mockGetBlockTransactionCountByNumberResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x2" + } +` + +func TestBaseClient_GetBlockTransactionCountByNumber(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetBlockTransactionCountByNumberRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetBlockTransactionCountByNumberResponse)), + }, nil + } + + transactionCount, err := client.GetBlockTransactionCountByNumber( + context.Background(), + types.MustBlockNumberFromHex("0x1"), + ) + + require.NoError(t, err) + assert.Equal(t, uint64(2), transactionCount) +} + +const mockGetUncleByBlockHashAndIndexRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getUncleByBlockHashAndIndex", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111", + "0x0" + ] + } +` + +func TestBaseClient_GetUncleByBlockHashAndIndex(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetUncleByBlockHashAndIndexRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockBlockResponse)), + }, nil + } + + block, err := client.GetUncleByBlockHashAndIndex( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + 0, + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) +} + +const mockGetUncleByBlockNumberAndIndexRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getUncleByBlockNumberAndIndex", + "params": [ + "0x1", + "0x2" + ] + } +` + +func TestBaseClient_GetUncleByBlockNumberAndIndex(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetUncleByBlockNumberAndIndexRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockBlockResponse)), + }, nil + } + + block, err := client.GetUncleByBlockNumberAndIndex( + context.Background(), + types.MustBlockNumberFromHex("0x1"), + 2, + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), block.Hash) +} + +const mockGetUncleCountByBlockHashRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getUncleCountByBlockHash", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111" + ] + } +` + +const mockGetUncleCountByBlockHashResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_GetUncleCountByBlockHash(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetUncleCountByBlockHashRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetUncleCountByBlockHashResponse)), + }, nil + } + + uncleCount, err := client.GetUncleCountByBlockHash( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + ) + + require.NoError(t, err) + assert.Equal(t, uint64(1), uncleCount) +} + +const mockGetUncleCountByBlockNumberRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getUncleCountByBlockNumber", + "params": [ + "0x1" + ] + } +` + +const mockGetUncleCountByBlockNumberResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x2" + } +` + +func TestBaseClient_GetUncleCountByBlockNumber(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetUncleCountByBlockNumberRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetUncleCountByBlockNumberResponse)), + }, nil + } + + uncleCount, err := client.GetUncleCountByBlockNumber( + context.Background(), + types.MustBlockNumberFromHex("0x1"), + ) + + require.NoError(t, err) + assert.Equal(t, uint64(2), uncleCount) +} + +// TODO: SubscribeNewHeads + +const mockCallRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_call", + "params": [ + { + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x76c0", + "gasPrice": "0x9184e72a000", + "value": "0x2540be400", + "input": "0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333" + }, + "0x1" + ] + } +` + +const mockCallResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000d9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f300000000000000000000000078d1ad571a1a09d60d9bbf25894b44e4c8859595000000000000000000000000286834935f4a8cfb4ff4c77d5770c2775ae2b0e7000000000000000000000000b86e2b0ab5a4b1373e40c51a7c712c70ba2f9f8e" + } +` + +func TestBaseClient_Call(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockCallRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockCallResponse)), + }, nil + } + + from := types.MustAddressFromHexPtr("0x1111111111111111111111111111111111111111") + to := types.MustAddressFromHexPtr("0x2222222222222222222222222222222222222222") + gasLimit := uint64(30400) + gasPrice := big.NewInt(10000000000000) + value := big.NewInt(10000000000) + input := hexutil.MustHexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333") + response, err := client.Call( + context.Background(), + &types.CallLegacy{ + CallData: types.CallData{ + From: from, + To: to, + GasLimit: &gasLimit, + Value: value, + Input: input, + }, + LegacyPriceData: types.LegacyPriceData{ + GasPrice: gasPrice, + }, + }, + types.MustBlockNumberFromHex("0x1"), + ) + + require.NoError(t, err) + assert.Equal(t, hexutil.MustHexToBytes("0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000d9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f300000000000000000000000078d1ad571a1a09d60d9bbf25894b44e4c8859595000000000000000000000000286834935f4a8cfb4ff4c77d5770c2775ae2b0e7000000000000000000000000b86e2b0ab5a4b1373e40c51a7c712c70ba2f9f8e"), response) +} + +const mockEstimateGasRequest = ` + { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + { + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x76c0", + "gasPrice": "0x9184e72a000", + "value": "0x2540be400", + "input": "0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333" + }, + "latest" + ] + } +` + +const mockEstimateGasResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x5208" + } +` + +func TestBaseClient_EstimateGas(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockEstimateGasRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockEstimateGasResponse)), + }, nil + } + + gasLimit := uint64(30400) + gas, err := client.EstimateGas( + context.Background(), + &types.CallLegacy{ + CallData: types.CallData{ + From: types.MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: types.MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + GasLimit: &gasLimit, + Value: big.NewInt(10000000000), + Input: hexutil.MustHexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333333333333333333333"), + }, + LegacyPriceData: types.LegacyPriceData{ + GasPrice: big.NewInt(10000000000000), + }, + }, + types.LatestBlockNumber, + ) + + require.NoError(t, err) + assert.Equal(t, uint64(21000), gas) +} + +const mockSendRawTransactionRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sendRawTransaction", + "params": [ + "0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333" + ] + } +` + +const mockSendRawTransactionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1111111111111111111111111111111111111111111111111111111111111111" + } +` + +func TestBaseClient_SendRawTransaction(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockSendRawTransactionRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockSendRawTransactionResponse)), + }, nil + } + + txHash, err := client.SendRawTransaction( + context.Background(), + hexutil.MustHexToBytes("0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333"), + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), *txHash) +} + +const mockGetTransactionByHashRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getTransactionByHash", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111" + ] + } +` + +func TestBaseClient_GetTransactionByHash(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetTransactionByHashRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockOnChainTransactionResponse)), + }, nil + } + + onChainTX, err := client.GetTransactionByHash( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), onChainTX.BlockHash) + assert.Equal(t, big.NewInt(0x22), onChainTX.BlockNumber) + assert.Equal(t, uint64(0x0), *onChainTX.TransactionIndex) + assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), onChainTX.Hash) + + tx := onChainTX.Transaction.(*types.TransactionLegacy) + assert.Equal(t, types.MustAddressFromHexPtr("0x3333333333333333333333333333333333333333"), tx.From) + assert.Equal(t, types.MustAddressFromHexPtr("0x7777777777777777777777777777777777777777"), tx.To) + assert.Equal(t, big.NewInt(10000000000), tx.Value) + assert.Equal(t, uint64(30400), *tx.GasLimit) + assert.Equal(t, big.NewInt(10000000000000), tx.GasPrice) + assert.Equal(t, hexutil.MustHexToBytes("0x555555555555"), tx.Input) + assert.Equal(t, uint64(0x66), *tx.Nonce) + assert.Equal(t, uint64(0x88), tx.Signature.V.Uint64()) + assert.Equal(t, hexutil.MustHexToBytes("0x9999999999999999999999999999999999999999999999999999999999999999"), tx.Signature.R.Bytes()) + assert.Equal(t, hexutil.MustHexToBytes("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), tx.Signature.S.Bytes()) +} + +const mockGetTransactionByBlockHashAndIndexRequest = ` + { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111", + "0x0" + ] + } +` + +func TestBaseClient_GetTransactionByBlockHashAndIndex(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetTransactionByBlockHashAndIndexRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockOnChainTransactionResponse)), + }, nil + } + + onChainTX, err := client.GetTransactionByBlockHashAndIndex( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + 0, + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), onChainTX.BlockHash) + assert.Equal(t, big.NewInt(0x22), onChainTX.BlockNumber) + assert.Equal(t, uint64(0x0), *onChainTX.TransactionIndex) + assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), onChainTX.Hash) +} + +const mockGetTransactionByBlockNumberAndIndexRequest = ` + { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "0x1", + "0x2" + ] + } +` + +func TestBaseClient_GetTransactionByBlockNumberAndIndex(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetTransactionByBlockNumberAndIndexRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockOnChainTransactionResponse)), + }, nil + } + + onChainTX, err := client.GetTransactionByBlockNumberAndIndex( + context.Background(), + types.MustBlockNumberFromHex("0x1"), + 2, + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), onChainTX.BlockHash) + assert.Equal(t, big.NewInt(0x22), onChainTX.BlockNumber) + assert.Equal(t, uint64(0x0), *onChainTX.TransactionIndex) + assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), onChainTX.Hash) +} + +const mockGetTransactionReceiptRequest = ` + { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111" + ] + } +` + +const mockGetTransactionReceiptResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockNumber": "0x2222", + "contractAddress": null, + "cumulativeGasUsed": "0x33333", + "effectiveGasPrice":"0x4444444444", + "from": "0x5555555555555555555555555555555555555555", + "gasUsed": "0x66666", + "logs": [ + { + "address": "0x7777777777777777777777777777777777777777", + "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockNumber": "0x2222", + "data": "0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35", + "logIndex": "0x8", + "removed": false, + "topics": [ + "0x9999999999999999999999999999999999999999999999999999999999999999" + ], + "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "transactionIndex": "0x11" + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000", + "status": "0x1", + "to": "0x7777777777777777777777777777777777777777", + "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "transactionIndex": "0x11", + "type": "0x0" + } + } +` + +func TestBaseClient_GetTransactionReceipt(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetTransactionReceiptRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetTransactionReceiptResponse)), + }, nil + } + + receipt, err := client.GetTransactionReceipt( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + ) + + status := uint64(1) + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipt.TransactionHash) + assert.Equal(t, uint64(17), receipt.TransactionIndex) + assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), receipt.BlockHash) + assert.Equal(t, big.NewInt(0x2222), receipt.BlockNumber) + assert.Equal(t, (*types.Address)(nil), receipt.ContractAddress) + assert.Equal(t, hexutil.MustHexToBigInt("0x33333").Uint64(), receipt.CumulativeGasUsed) + assert.Equal(t, hexutil.MustHexToBigInt("0x4444444444"), receipt.EffectiveGasPrice) + assert.Equal(t, hexutil.MustHexToBigInt("0x66666").Uint64(), receipt.GasUsed) + assert.Equal(t, types.MustAddressFromHex("0x5555555555555555555555555555555555555555"), receipt.From) + assert.Equal(t, types.MustAddressFromHex("0x7777777777777777777777777777777777777777"), receipt.To) + assert.Equal(t, hexutil.MustHexToBytes("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000"), receipt.LogsBloom) + assert.Equal(t, &status, receipt.Status) + require.Len(t, receipt.Logs, 1) + assert.Equal(t, types.MustHashFromHexPtr("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipt.Logs[0].TransactionHash) + assert.Equal(t, uint64(17), *receipt.Logs[0].TransactionIndex) + assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), receipt.Logs[0].BlockHash) + assert.Equal(t, big.NewInt(0x2222), receipt.Logs[0].BlockNumber) + assert.Equal(t, uint64(8), *receipt.Logs[0].LogIndex) + assert.Equal(t, hexutil.MustHexToBytes("0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35"), receipt.Logs[0].Data) + assert.Equal(t, types.MustAddressFromHex("0x7777777777777777777777777777777777777777"), receipt.Logs[0].Address) + assert.Equal(t, []types.Hash{types.MustHashFromHex("0x9999999999999999999999999999999999999999999999999999999999999999", types.PadNone)}, receipt.Logs[0].Topics) + assert.Equal(t, false, receipt.Logs[0].Removed) +} + +const mockGetBlockReceiptsRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getBlockReceipts", + "params": [ + "0x1" + ] + } +` + +const mockGetBlockReceiptsResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockNumber": "0x2222", + "contractAddress": null, + "cumulativeGasUsed": "0x33333", + "effectiveGasPrice": "0x4444444444", + "from": "0x5555555555555555555555555555555555555555", + "gasUsed": "0x66666", + "logs": [ + { + "address": "0x7777777777777777777777777777777777777777", + "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockNumber": "0x2222", + "data": "0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35", + "logIndex": "0x8", + "removed": false, + "topics": [ + "0x9999999999999999999999999999999999999999999999999999999999999999" + ], + "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "transactionIndex": "0x11" + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000", + "status": "0x1", + "to": "0x7777777777777777777777777777777777777777", + "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "transactionIndex": "0x11", + "type": "0x0" + } + ] + } +` + +func TestBaseClient_GetBlockReceipts(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetBlockReceiptsRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetBlockReceiptsResponse)), + }, nil + } + + receipts, err := client.GetBlockReceipts( + context.Background(), + types.MustBlockNumberFromHex("0x1"), + ) + + require.NoError(t, err) + require.Len(t, receipts, 1) + assert.Equal(t, types.MustHashFromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipts[0].TransactionHash) + assert.Equal(t, uint64(17), receipts[0].TransactionIndex) + assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), receipts[0].BlockHash) + assert.Equal(t, big.NewInt(0x2222), receipts[0].BlockNumber) + assert.Equal(t, (*types.Address)(nil), receipts[0].ContractAddress) + assert.Equal(t, hexutil.MustHexToBigInt("0x33333").Uint64(), receipts[0].CumulativeGasUsed) + assert.Equal(t, hexutil.MustHexToBigInt("0x4444444444"), receipts[0].EffectiveGasPrice) + assert.Equal(t, hexutil.MustHexToBigInt("0x66666").Uint64(), receipts[0].GasUsed) + assert.Equal(t, types.MustAddressFromHex("0x5555555555555555555555555555555555555555"), receipts[0].From) + assert.Equal(t, types.MustAddressFromHex("0x7777777777777777777777777777777777777777"), receipts[0].To) + assert.Equal(t, hexutil.MustHexToBytes("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000"), receipts[0].LogsBloom) + status := uint64(1) + assert.Equal(t, &status, receipts[0].Status) + require.Len(t, receipts[0].Logs, 1) + assert.Equal(t, types.MustHashFromHexPtr("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", types.PadNone), receipts[0].Logs[0].TransactionHash) + assert.Equal(t, uint64(17), *receipts[0].Logs[0].TransactionIndex) + assert.Equal(t, types.MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), receipts[0].Logs[0].BlockHash) + assert.Equal(t, big.NewInt(0x2222), receipts[0].Logs[0].BlockNumber) + assert.Equal(t, uint64(8), *receipts[0].Logs[0].LogIndex) + assert.Equal(t, hexutil.MustHexToBytes("0x000000000000000000000000398137383b3d25c92898c656696e41950e47316b00000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e100000000000000000000000000000000000000000000000000000000005baf35"), receipts[0].Logs[0].Data) + assert.Equal(t, types.MustAddressFromHex("0x7777777777777777777777777777777777777777"), receipts[0].Logs[0].Address) + assert.Equal(t, []types.Hash{types.MustHashFromHex("0x9999999999999999999999999999999999999999999999999999999999999999", types.PadNone)}, receipts[0].Logs[0].Topics) + assert.Equal(t, false, receipts[0].Logs[0].Removed) +} + +// TODO: SubscribeNewPendingTransactions + +const mockGetLogsRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getLogs", + "params": [ + { + "fromBlock": "0x1", + "toBlock": "0x2", + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0x4444444444444444444444444444444444444444444444444444444444444444" + ] + } + ] + } +` + +const mockGetLogsResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0x4444444444444444444444444444444444444444444444444444444444444444" + ], + "data": "0x68656c6c6f21", + "blockNumber": "0x1", + "transactionHash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "transactionIndex": "0x0", + "blockHash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "logIndex": "0x0", + "removed": false + } + ] + } +` + +func TestBaseClient_GetLogs(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetLogsRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetLogsResponse)), + }, nil + } + + from := types.MustBlockNumberFromHex("0x1") + to := types.MustBlockNumberFromHex("0x2") + logs, err := client.GetLogs(context.Background(), &types.FilterLogsQuery{ + FromBlock: &from, + ToBlock: &to, + Address: []types.Address{types.MustAddressFromHex("0x3333333333333333333333333333333333333333")}, + Topics: [][]types.Hash{ + {types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone)}, + }, + }) + + require.NoError(t, err) + require.Len(t, logs, 1) + assert.Equal(t, types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), logs[0].Address) + assert.Equal(t, types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].Topics[0]) + assert.Equal(t, hexutil.MustHexToBytes("0x68656c6c6f21"), logs[0].Data) + assert.Equal(t, big.NewInt(1), logs[0].BlockNumber) + assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].TransactionHash) + assert.Equal(t, uint64(0), *logs[0].TransactionIndex) + assert.Equal(t, types.MustHashFromHexPtr("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].BlockHash) + assert.Equal(t, uint64(0), *logs[0].LogIndex) + assert.Equal(t, false, logs[0].Removed) +} + +// TODO: SubscribeLogs + +const mockChanIDRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_chainId", + "params": [] + } +` + +const mockChanIDResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_ChainID(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockChanIDRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockChanIDResponse)), + }, nil + } + + chainID, err := client.ChainID(context.Background()) + require.NoError(t, err) + assert.Equal(t, uint64(1), chainID) +} + +const mockBlockNumberRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_blockNumber", + "params": [] + } +` + +const mockBlockNumberResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_BlockNumber(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockBlockNumberRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockBlockNumberResponse)), + }, nil + } + + blockNumber, err := client.BlockNumber(context.Background()) + + require.NoError(t, err) + assert.Equal(t, big.NewInt(1), blockNumber) +} + +const mockGasPriceRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_gasPrice", + "params": [] + } +` + +const mockGasPriceResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x09184e72a000" + } +` + +func TestBaseClient_GasPrice(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGasPriceRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGasPriceResponse)), + }, nil + } + + gasPrice, err := client.GasPrice(context.Background()) + + require.NoError(t, err) + assert.Equal(t, big.NewInt(10000000000000), gasPrice) +} + +const mockMaxPriorityFeePerGasRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_maxPriorityFeePerGas", + "params": [] + } +` + +const mockMaxPriorityFeePerGasResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_MaxPriorityFeePerGas(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockMaxPriorityFeePerGasRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockMaxPriorityFeePerGasResponse)), + }, nil + } + + gasPrice, err := client.MaxPriorityFeePerGas(context.Background()) + + require.NoError(t, err) + assert.Equal(t, hexutil.MustHexToBigInt("0x1"), gasPrice) +} + +const mockBlobBaseFeeRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_blobBaseFee", + "params": [] + } +` + +const mockBlobBaseFeeResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_BlobBaseFee(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockBlobBaseFeeRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockBlobBaseFeeResponse)), + }, nil + } + + gasPrice, err := client.BlobBaseFee(context.Background()) + + require.NoError(t, err) + assert.Equal(t, hexutil.MustHexToBigInt("0x1"), gasPrice) +} + +func TestBaseClient_SubscribeNewHeads(t *testing.T) { + // TODO: Veirify + + streamMock := newStreamMock(t) + client := &MethodsCommon{Transport: streamMock} + + ch := make(chan json.RawMessage) + streamMock.SubscribeMocks = []subscribeMock{ + { + ArgMethod: "newHeads", + ArgParams: []any{}, + RetCh: ch, + RetID: "0x1", + RetErr: nil, + }, + } + streamMock.UnsubscribeMocks = []unsubscribeMock{ + { + ArgID: "0x1", + ResultErr: nil, + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + resultCh, err := client.SubscribeNewHeads(ctx) + require.NoError(t, err) + assert.NotNil(t, resultCh) +} + +func TestBaseClient_SubscribeNewPendingTransactions(t *testing.T) { + // TODO: Veirify + + streamMock := newStreamMock(t) + client := &MethodsCommon{Transport: streamMock} + + ch := make(chan json.RawMessage) + streamMock.SubscribeMocks = []subscribeMock{ + { + ArgMethod: "newPendingTransactions", + ArgParams: []any{}, + RetCh: ch, + RetID: "0x2", + RetErr: nil, + }, + } + streamMock.UnsubscribeMocks = []unsubscribeMock{ + { + ArgID: "0x2", + ResultErr: nil, + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + resultCh, err := client.SubscribeNewPendingTransactions(ctx) + require.NoError(t, err) + assert.NotNil(t, resultCh) +} + +func TestBaseClient_SubscribeLogs(t *testing.T) { + // TODO: Veirify + + streamMock := newStreamMock(t) + client := &MethodsCommon{Transport: streamMock} + + ch := make(chan json.RawMessage) + from := types.MustBlockNumberFromHex("0x1") + to := types.MustBlockNumberFromHex("0x2") + query := &types.FilterLogsQuery{ + FromBlock: &from, + ToBlock: &to, + Address: []types.Address{types.MustAddressFromHex("0x3333333333333333333333333333333333333333")}, + Topics: [][]types.Hash{ + {types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone)}, + }, + } + streamMock.SubscribeMocks = []subscribeMock{ + { + ArgMethod: "logs", + ArgParams: []any{query}, + RetCh: ch, + RetID: "0x3", + RetErr: nil, + }, + } + streamMock.UnsubscribeMocks = []unsubscribeMock{ + { + ArgID: "0x3", + ResultErr: nil, + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + resultCh, err := client.SubscribeLogs(ctx, query) + require.NoError(t, err) + assert.NotNil(t, resultCh) +} diff --git a/rpc/methods_filter.go b/rpc/methods_filter.go new file mode 100644 index 0000000..2f525b9 --- /dev/null +++ b/rpc/methods_filter.go @@ -0,0 +1,80 @@ +package rpc + +import ( + "context" + "math/big" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// MethodsFilter is a collection of RPC methods to interact with filters. +// +// Note: Some JSON-RPC APIs do not support these methods. +type MethodsFilter struct { + Transport transport.Transport +} + +// NewFilter implements the RPC interface. +func (c *MethodsFilter) NewFilter(ctx context.Context, query *types.FilterLogsQuery) (*big.Int, error) { + var res *types.Number + if err := c.Transport.Call(ctx, &res, "eth_newFilter", query); err != nil { + return nil, err + } + return res.Big(), nil +} + +// NewBlockFilter implements the RPC interface. +func (c *MethodsFilter) NewBlockFilter(ctx context.Context) (*big.Int, error) { + var res *types.Number + if err := c.Transport.Call(ctx, &res, "eth_newBlockFilter"); err != nil { + return nil, err + } + return res.Big(), nil + +} + +// NewPendingTransactionFilter implements the RPC interface. +func (c *MethodsFilter) NewPendingTransactionFilter(ctx context.Context) (*big.Int, error) { + var res *types.Number + if err := c.Transport.Call(ctx, &res, "eth_newPendingTransactionFilter"); err != nil { + return nil, err + } + return res.Big(), nil +} + +// UninstallFilter implements the RPC interface. +func (c *MethodsFilter) UninstallFilter(ctx context.Context, id *big.Int) (bool, error) { + var res bool + if err := c.Transport.Call(ctx, &res, "eth_uninstallFilter", types.NumberFromBigInt(id)); err != nil { + return false, err + } + return res, nil +} + +// GetFilterChanges implements the RPC interface. +func (c *MethodsFilter) GetFilterChanges(ctx context.Context, id *big.Int) ([]types.Log, error) { + var res []types.Log + if err := c.Transport.Call(ctx, &res, "eth_getFilterChanges", types.NumberFromBigInt(id)); err != nil { + return nil, err + } + return res, nil +} + +// GetFilterLogs implements the RPC interface. +func (c *MethodsFilter) GetFilterLogs(ctx context.Context, id *big.Int) ([]types.Log, error) { + var res []types.Log + if err := c.Transport.Call(ctx, &res, "eth_getFilterLogs", types.NumberFromBigInt(id)); err != nil { + return nil, err + } + return res, nil +} + +// GetBlockFilterChanges implements the RPC interface. +func (c *MethodsFilter) GetBlockFilterChanges(ctx context.Context, id *big.Int) ([]types.Hash, error) { + var res []types.Hash + if err := c.Transport.Call(ctx, &res, "eth_getFilterChanges", types.NumberFromBigInt(id)); err != nil { + return nil, err + } + return res, nil +} diff --git a/rpc/methods_filter_test.go b/rpc/methods_filter_test.go new file mode 100644 index 0000000..1055bf3 --- /dev/null +++ b/rpc/methods_filter_test.go @@ -0,0 +1,314 @@ +package rpc + +import ( + "bytes" + "context" + "io" + "math/big" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/types" +) + +const mockNewFilterRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_newFilter", + "params": [ + { + "fromBlock": "0x1", + "toBlock": "0x2", + "address": "0x3333333333333333333333333333333333333333", + "topics": ["0x4444444444444444444444444444444444444444444444444444444444444444"] + } + ] + } +` + +const mockNewFilterResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" + } +` + +func TestBaseClient_NewFilter(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockNewFilterRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockNewFilterResponse)), + }, nil + } + + from := types.MustBlockNumberFromHex("0x1") + to := types.MustBlockNumberFromHex("0x2") + id, err := client.NewFilter(context.Background(), &types.FilterLogsQuery{ + FromBlock: &from, + ToBlock: &to, + Address: []types.Address{types.MustAddressFromHex("0x3333333333333333333333333333333333333333")}, + Topics: [][]types.Hash{ + {types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone)}, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "1", id.String()) +} + +const mockNewBlockFilterRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_newBlockFilter", + "params": [] + } +` + +const mockNewBlockFilterResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x2" + } +` + +func TestBaseClient_NewBlockFilter(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockNewBlockFilterRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockNewBlockFilterResponse)), + }, nil + } + + id, err := client.NewBlockFilter(context.Background()) + + require.NoError(t, err) + assert.Equal(t, "2", id.String()) +} + +const mockNewPendingTransactionFilterRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_newPendingTransactionFilter", + "params": [] + } +` + +const mockNewPendingTransactionFilterResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x3" + } +` + +func TestBaseClient_NewPendingTransactionFilter(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockNewPendingTransactionFilterRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockNewPendingTransactionFilterResponse)), + }, nil + } + + id, err := client.NewPendingTransactionFilter(context.Background()) + + require.NoError(t, err) + assert.Equal(t, "3", id.String()) +} + +const mockUninstallFilterRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_uninstallFilter", + "params": ["0x1"] + } +` + +const mockUninstallFilterResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": true + } +` + +func TestBaseClient_UninstallFilter(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockUninstallFilterRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockUninstallFilterResponse)), + }, nil + } + + result, err := client.UninstallFilter(context.Background(), big.NewInt(1)) + + require.NoError(t, err) + assert.True(t, result) +} + +const mockGetFilterChangesRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getFilterChanges", + "params": ["0x1"] + } +` + +const mockGetFilterChangesResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0x4444444444444444444444444444444444444444444444444444444444444444" + ], + "data": "0x68656c6c6f21", + "blockNumber": "0x1", + "transactionHash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "transactionIndex": "0x0", + "blockHash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "logIndex": "0x0", + "removed": false + } + ] + } +` + +func TestBaseClient_GetFilterChanges(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetFilterChangesRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetFilterChangesResponse)), + }, nil + } + + logs, err := client.GetFilterChanges(context.Background(), big.NewInt(1)) + + require.NoError(t, err) + require.Len(t, logs, 1) + assert.Equal(t, types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), logs[0].Address) + assert.Equal(t, types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].Topics[0]) +} + +const mockGetFilterLogsRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getFilterLogs", + "params": ["0x1"] + } +` + +const mockGetFilterLogsResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0x4444444444444444444444444444444444444444444444444444444444444444" + ], + "data": "0x68656c6c6f21", + "blockNumber": "0x2", + "transactionHash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "transactionIndex": "0x1", + "blockHash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "logIndex": "0x1", + "removed": false + } + ] + } +` + +func TestBaseClient_GetFilterLogs(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetFilterLogsRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetFilterLogsResponse)), + }, nil + } + + logs, err := client.GetFilterLogs(context.Background(), big.NewInt(1)) + + require.NoError(t, err) + require.Len(t, logs, 1) + assert.Equal(t, types.MustAddressFromHex("0x3333333333333333333333333333333333333333"), logs[0].Address) + assert.Equal(t, types.MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", types.PadNone), logs[0].Topics[0]) + assert.Equal(t, big.NewInt(2), logs[0].BlockNumber) +} + +const mockGetBlockFilterChangesRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_getFilterChanges", + "params": ["0x2"] + } +` + +const mockGetBlockFilterChangesResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": [ + "0x1111111111111111111111111111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222222222222222222222222222" + ] + } +` + +func TestBaseClient_GetBlockFilterChanges(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsFilter{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockGetBlockFilterChangesRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockGetBlockFilterChangesResponse)), + }, nil + } + + hashes, err := client.GetBlockFilterChanges(context.Background(), big.NewInt(2)) + + require.NoError(t, err) + require.Len(t, hashes, 2) + assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), hashes[0]) + assert.Equal(t, types.MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", types.PadNone), hashes[1]) +} diff --git a/rpc/methods_privatetransactions.go b/rpc/methods_privatetransactions.go new file mode 100644 index 0000000..4905b65 --- /dev/null +++ b/rpc/methods_privatetransactions.go @@ -0,0 +1,29 @@ +package rpc + +import ( + "context" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// MethodsPrivateTransaction implements unofficial RPC methods for private transactions. +type MethodsPrivateTransaction struct { + Transport transport.Transport +} + +func (c *MethodsPrivateTransaction) CancelPrivateTransaction(ctx context.Context, hash types.Hash) (bool, error) { + var res bool + if err := c.Transport.Call(ctx, &res, "eth_cancelPrivateTransaction", hash); err != nil { + return false, err + } + return res, nil +} + +func (c *MethodsPrivateTransaction) SendPrivateTransaction(ctx context.Context, data []byte) (*types.Hash, error) { + var res types.Hash + if err := c.Transport.Call(ctx, &res, "eth_sendPrivateTransaction", types.Bytes(data)); err != nil { + return nil, err + } + return &res, nil +} diff --git a/rpc/methods_privatetransactions_test.go b/rpc/methods_privatetransactions_test.go new file mode 100644 index 0000000..b125e7c --- /dev/null +++ b/rpc/methods_privatetransactions_test.go @@ -0,0 +1,95 @@ +package rpc + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/types" +) + +const mockCancelPrivateTransactionRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_cancelPrivateTransaction", + "params": [ + "0x1111111111111111111111111111111111111111111111111111111111111111" + ] + } +` + +const mockCancelPrivateTransactionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": true + } +` + +func TestBaseClient_CancelPrivateTransaction(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsPrivateTransaction{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockCancelPrivateTransactionRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockCancelPrivateTransactionResponse)), + }, nil + } + + result, err := client.CancelPrivateTransaction( + context.Background(), + types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), + ) + + require.NoError(t, err) + assert.True(t, result) +} + +const mockSendPrivateTransactionRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sendPrivateTransaction", + "params": [ + "0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333" + ] + } +` + +const mockSendPrivateTransactionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1111111111111111111111111111111111111111111111111111111111111111" + } +` + +func TestBaseClient_SendPrivateTransaction(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsPrivateTransaction{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockSendPrivateTransactionRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockSendPrivateTransactionResponse)), + }, nil + } + + txHash, err := client.SendPrivateTransaction( + context.Background(), + hexutil.MustHexToBytes("0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333"), + ) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), *txHash) +} diff --git a/rpc/methods_wallet.go b/rpc/methods_wallet.go new file mode 100644 index 0000000..f1339ef --- /dev/null +++ b/rpc/methods_wallet.go @@ -0,0 +1,67 @@ +package rpc + +import ( + "context" + "errors" + + "github.com/defiweb/go-eth/rpc/transport" + "github.com/defiweb/go-eth/types" +) + +// MethodsWallet is a collection of RPC methods that require private keys to +// perform operations. +// +// Note: Public JSON-RPC APIs do not support these methods. +type MethodsWallet struct { + Transport transport.Transport +} + +// Accounts performs eth_accounts RPC call. +// +// It returns the list of addresses owned by the client. +func (c *MethodsWallet) Accounts(ctx context.Context) ([]types.Address, error) { + var res []types.Address + if err := c.Transport.Call(ctx, &res, "eth_accounts"); err != nil { + return nil, err + } + return res, nil +} + +// Sign performs eth_sign RPC call. +// +// It signs the given data with the given address. +func (c *MethodsWallet) Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) { + var res types.Signature + if err := c.Transport.Call(ctx, &res, "eth_sign", account, types.Bytes(data)); err != nil { + return nil, err + } + return &res, nil +} + +// SignTransaction performs eth_signTransaction RPC call. +// +// It signs the given transaction and returns the raw transaction data. +func (c *MethodsWallet) SignTransaction(ctx context.Context, tx types.Transaction) ([]byte, error) { + if tx == nil { + return nil, errors.New("rpc client: transaction is nil") + } + var res signTransactionResult + if err := c.Transport.Call(ctx, &res, "eth_signTransaction", tx); err != nil { + return nil, err + } + return res.Raw, nil +} + +// SendTransaction performs eth_sendTransaction RPC call. +// +// It sends a transaction to the network. +func (c *MethodsCommon) SendTransaction(ctx context.Context, tx types.Transaction) (*types.Hash, error) { + if tx == nil { + return nil, errors.New("rpc client: transaction is nil") + } + var res types.Hash + if err := c.Transport.Call(ctx, &res, "eth_sendTransaction", tx); err != nil { + return nil, err + } + return &res, nil +} diff --git a/rpc/methods_wallet_test.go b/rpc/methods_wallet_test.go new file mode 100644 index 0000000..6a8ca1a --- /dev/null +++ b/rpc/methods_wallet_test.go @@ -0,0 +1,203 @@ +package rpc + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" + "github.com/defiweb/go-eth/types" +) + +const mockAccountsRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_accounts", + "params": [] + } +` + +const mockAccountsResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222" + ] + } +` + +func TestBaseClient_Accounts(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsWallet{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockAccountsRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockAccountsResponse)), + }, nil + } + + accounts, err := client.Accounts(context.Background()) + + require.NoError(t, err) + require.Len(t, accounts, 2) + assert.Equal(t, types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), accounts[0]) + assert.Equal(t, types.MustAddressFromHex("0x2222222222222222222222222222222222222222"), accounts[1]) +} + +const mockSignRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sign", + "params": [ + "0x1111111111111111111111111111111111111111", + "0x48656c6c6f20576f726c64" + ] + } +` + +const mockSignResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x3333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444455" + } +` + +func TestBaseClient_Sign(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsWallet{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockSignRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockSignResponse)), + }, nil + } + + signature, err := client.Sign( + context.Background(), + types.MustAddressFromHex("0x1111111111111111111111111111111111111111"), + []byte("Hello World"), + ) + + require.NoError(t, err) + assert.Equal(t, hexutil.MustHexToBytes("0x3333333333333333333333333333333333333333333333333333333333333333"), signature.R.Bytes()) + assert.Equal(t, hexutil.MustHexToBytes("0x4444444444444444444444444444444444444444444444444444444444444444"), signature.S.Bytes()) + assert.Equal(t, uint64(0x55), signature.V.Uint64()) +} + +const mockSignTransactionRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_signTransaction", + "params": [ + { + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x5208", + "gasPrice": "0x9184e72a000", + "value": "0x2540be400", + "input": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" + } + ] + } +` + +const mockSignTransactionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "raw": "0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333" + } + } +` + +func TestBaseClient_SignTransaction(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsWallet{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockSignTransactionRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockSignTransactionResponse)), + }, nil + } + + tx := types.NewTransactionLegacy() + tx.SetFrom(*types.MustAddressFromHexPtr("0x1111111111111111111111111111111111111111")) + tx.SetTo(*types.MustAddressFromHexPtr("0x2222222222222222222222222222222222222222")) + tx.SetValue(hexutil.MustHexToBigInt("0x2540be400")) + tx.SetGasLimit(0x5208) + tx.SetGasPrice(hexutil.MustHexToBigInt("0x9184e72a000")) + tx.SetInput(hexutil.MustHexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675")) + rawTx, err := client.SignTransaction(context.Background(), tx) + + require.NoError(t, err) + assert.Equal(t, "0xf893808609184e72a0008276c094d46e8dd67c5d32be8058bb8eb970870f072445678502540be400a9d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f07244567511a02222222222222222222222222222222222222222222222222222222222222222a03333333333333333333333333333333333333333333333333333333333333333", hexutil.BytesToHex(rawTx)) +} + +const mockSendTransactionRequest = ` + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sendTransaction", + "params": [ + { + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x5208", + "gasPrice": "0x9184e72a000", + "value": "0x2540be400", + "input": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675" + } + ] + } +` + +const mockSendTransactionResponse = ` + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x1111111111111111111111111111111111111111111111111111111111111111" + } +` + +func TestBaseClient_SendTransaction(t *testing.T) { + httpMock := newHTTPMock() + client := &MethodsCommon{Transport: httpMock} + + httpMock.Handler = func(req *http.Request) (*http.Response, error) { + assert.JSONEq(t, mockSendTransactionRequest, readBody(req)) + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(mockSendTransactionResponse)), + }, nil + } + + tx := types.NewTransactionLegacy() + tx.SetFrom(*types.MustAddressFromHexPtr("0x1111111111111111111111111111111111111111")) + tx.SetTo(*types.MustAddressFromHexPtr("0x2222222222222222222222222222222222222222")) + tx.SetValue(hexutil.MustHexToBigInt("0x2540be400")) + tx.SetGasLimit(0x5208) + tx.SetGasPrice(hexutil.MustHexToBigInt("0x9184e72a000")) + tx.SetInput(hexutil.MustHexToBytes("0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675")) + txHash, err := client.SendTransaction(context.Background(), tx) + + require.NoError(t, err) + assert.Equal(t, types.MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", types.PadNone), *txHash) +} diff --git a/rpc/mocks_test.go b/rpc/mocks_test.go index 24fdab1..97d6382 100644 --- a/rpc/mocks_test.go +++ b/rpc/mocks_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "io" "net/http" "testing" @@ -22,8 +23,7 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { type httpMock struct { *transport.HTTP - Request *http.Request - ResponseMock *http.Response + Handler func(req *http.Request) (*http.Response, error) } func newHTTPMock() *httpMock { @@ -32,8 +32,7 @@ func newHTTPMock() *httpMock { URL: "http://localhost", HTTPClient: &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - h.Request = req - return h.ResponseMock, nil + return h.Handler(req) }), }, }) @@ -118,3 +117,8 @@ func (k *keyMock) VerifyHash(ctx context.Context, hash types.Hash, sig types.Sig func (k keyMock) VerifyMessage(ctx context.Context, data []byte, sig types.Signature) bool { return false } + +func readBody(r *http.Request) string { + body, _ := io.ReadAll(r.Body) + return string(body) +} diff --git a/rpc/rpc.go b/rpc/rpc.go index 1c5c03c..81c28f7 100644 --- a/rpc/rpc.go +++ b/rpc/rpc.go @@ -1,259 +1,3 @@ +// Package rpc provides a client for interacting with the Ethereum JSON-RPC +// API. package rpc - -import ( - "context" - "math/big" - - "github.com/defiweb/go-eth/types" -) - -// RPC is an RPC client for the Ethereum-compatible nodes. -type RPC interface { - // ClientVersion performs web3_clientVersion RPC call. - // - // It returns the current client version. - ClientVersion(ctx context.Context) (string, error) - - // Listening performs net_listening RPC call. - // - // It returns true if the client is actively listening for network. - Listening(ctx context.Context) (bool, error) - - // PeerCount performs net_peerCount RPC call. - // - // It returns the number of connected peers. - PeerCount(ctx context.Context) (uint64, error) - - // ProtocolVersion performs eth_protocolVersion RPC call. - // - // It returns the current Ethereum protocol version. - ProtocolVersion(ctx context.Context) (uint64, error) - - // Syncing performs eth_syncing RPC call. - // - // It returns an object with data about the sync status or false. - Syncing(ctx context.Context) (*types.SyncStatus, error) - - // NetworkID performs net_version RPC call. - // - // It returns the current network ID. - NetworkID(ctx context.Context) (uint64, error) - - // ChainID performs eth_chainId RPC call. - // - // It returns the current chain ID. - ChainID(ctx context.Context) (uint64, error) - - // GasPrice performs eth_gasPrice RPC call. - // - // It returns the current price per gas in wei. - GasPrice(ctx context.Context) (*big.Int, error) - - // Accounts performs eth_accounts RPC call. - // - // It returns the list of addresses owned by the client. - Accounts(ctx context.Context) ([]types.Address, error) - - // BlockNumber performs eth_blockNumber RPC call. - // - // It returns the current block number. - BlockNumber(ctx context.Context) (*big.Int, error) - - // GetBalance performs eth_getBalance RPC call. - // - // It returns the balance of the account of given address in wei. - GetBalance(ctx context.Context, address types.Address, block types.BlockNumber) (*big.Int, error) - - // GetStorageAt performs eth_getStorageAt RPC call. - // - // It returns the value of key in the contract storage at the given - // address. - GetStorageAt(ctx context.Context, account types.Address, key types.Hash, block types.BlockNumber) (*types.Hash, error) - - // GetTransactionCount performs eth_getTransactionCount RPC call. - // - // It returns the number of transactions sent from the given address. - GetTransactionCount(ctx context.Context, account types.Address, block types.BlockNumber) (uint64, error) - - // GetBlockTransactionCountByHash performs eth_getBlockTransactionCountByHash RPC call. - // - // It returns the number of transactions in the block with the given hash. - GetBlockTransactionCountByHash(ctx context.Context, hash types.Hash) (uint64, error) - - // GetBlockTransactionCountByNumber performs eth_getBlockTransactionCountByNumber RPC call. - // - // It returns the number of transactions in the block with the given block - GetBlockTransactionCountByNumber(ctx context.Context, number types.BlockNumber) (uint64, error) - - // GetUncleCountByBlockHash performs eth_getUncleCountByBlockHash RPC call. - // - // It returns the number of uncles in the block with the given hash. - GetUncleCountByBlockHash(ctx context.Context, hash types.Hash) (uint64, error) - - // GetUncleCountByBlockNumber performs eth_getUncleCountByBlockNumber RPC call. - // - // It returns the number of uncles in the block with the given block number. - GetUncleCountByBlockNumber(ctx context.Context, number types.BlockNumber) (uint64, error) - - // GetCode performs eth_getCode RPC call. - // - // It returns the contract code at the given address. - GetCode(ctx context.Context, account types.Address, block types.BlockNumber) ([]byte, error) - - // Sign performs eth_sign RPC call. - // - // It signs the given data with the given address. - Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) - - // SignTransaction performs eth_signTransaction RPC call. - // - // It signs the given transaction. - // - // If transaction was internally mutated, the mutated call is returned. - SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, *types.Transaction, error) - - // SendTransaction performs eth_sendTransaction RPC call. - // - // It sends a transaction to the network. - // - // If transaction was internally mutated, the mutated call is returned. - SendTransaction(ctx context.Context, tx *types.Transaction) (*types.Hash, *types.Transaction, error) - - // SendRawTransaction performs eth_sendRawTransaction RPC call. - // - // It sends an encoded transaction to the network. - SendRawTransaction(ctx context.Context, data []byte) (*types.Hash, error) - - // Call performs eth_call RPC call. - // - // It executes a new message call immediately without creating a - // transaction on the blockchain. - // - // If call was internally mutated, the mutated call is returned. - Call(ctx context.Context, call *types.Call, block types.BlockNumber) ([]byte, *types.Call, error) - - // EstimateGas performs eth_estimateGas RPC call. - // - // It estimates the gas necessary to execute a specific transaction. - // - // If call was internally mutated, the mutated call is returned. - EstimateGas(ctx context.Context, call *types.Call, block types.BlockNumber) (uint64, *types.Call, error) - - // BlockByHash performs eth_getBlockByHash RPC call. - // - // It returns information about a block by hash. - BlockByHash(ctx context.Context, hash types.Hash, full bool) (*types.Block, error) - - // BlockByNumber performs eth_getBlockByNumber RPC call. - // - // It returns the block with the given number. - BlockByNumber(ctx context.Context, number types.BlockNumber, full bool) (*types.Block, error) - - // GetTransactionByHash performs eth_getTransactionByHash RPC call. - // - // It returns the information about a transaction requested by transaction. - GetTransactionByHash(ctx context.Context, hash types.Hash) (*types.OnChainTransaction, error) - - // GetTransactionByBlockHashAndIndex performs eth_getTransactionByBlockHashAndIndex RPC call. - // - // It returns the information about a transaction requested by transaction. - GetTransactionByBlockHashAndIndex(ctx context.Context, hash types.Hash, index uint64) (*types.OnChainTransaction, error) - - // GetTransactionByBlockNumberAndIndex performs eth_getTransactionByBlockNumberAndIndex RPC call. - // - // It returns the information about a transaction requested by transaction. - GetTransactionByBlockNumberAndIndex(ctx context.Context, number types.BlockNumber, index uint64) (*types.OnChainTransaction, error) - - // GetTransactionReceipt performs eth_getTransactionReceipt RPC call. - // - // It returns the receipt of a transaction by transaction hash. - GetTransactionReceipt(ctx context.Context, hash types.Hash) (*types.TransactionReceipt, error) - - // GetBlockReceipts performs eth_getBlockReceipts RPC call. - // - // It returns all transaction receipts for a given block hash or number. - GetBlockReceipts(ctx context.Context, block types.BlockNumber) ([]*types.TransactionReceipt, error) - - // GetUncleByBlockHashAndIndex performs eth_getUncleByBlockNumberAndIndex RPC call. - // - // It returns information about an uncle of a block by number and uncle index position. - GetUncleByBlockHashAndIndex(ctx context.Context, hash types.Hash, index uint64) (*types.Block, error) - - // GetUncleByBlockNumberAndIndex performs eth_getUncleByBlockNumberAndIndex RPC call. - // - // It returns information about an uncle of a block by hash and uncle index position. - GetUncleByBlockNumberAndIndex(ctx context.Context, number types.BlockNumber, index uint64) (*types.Block, error) - - // NewFilter performs eth_newFilter RPC call. - // - // It creates a filter object based on the given filter options. To check - // if the state has changed, use GetFilterChanges. - NewFilter(ctx context.Context, query *types.FilterLogsQuery) (*big.Int, error) - - // NewBlockFilter performs eth_newBlockFilter RPC call. - // - // It creates a filter in the node, to notify when a new block arrives. To - // check if the state has changed, use GetBlockFilterChanges. - NewBlockFilter(ctx context.Context) (*big.Int, error) - - // NewPendingTransactionFilter performs eth_newPendingTransactionFilter RPC call. - // - // It creates a filter in the node, to notify when new pending transactions - // arrive. To check if the state has changed, use GetFilterChanges. - NewPendingTransactionFilter(ctx context.Context) (*big.Int, error) - - // UninstallFilter performs eth_uninstallFilter RPC call. - // - // It uninstalls a filter with given ID. Should always be called when watch - // is no longer needed. - UninstallFilter(ctx context.Context, id *big.Int) (bool, error) - - // GetFilterChanges performs eth_getFilterChanges RPC call. - // - // It returns an array of logs that occurred since the given filter ID. - GetFilterChanges(ctx context.Context, id *big.Int) ([]types.Log, error) - - // GetBlockFilterChanges performs eth_getFilterChanges RPC call. - // - // It returns an array of block hashes that occurred since the given filter ID. - GetBlockFilterChanges(ctx context.Context, id *big.Int) ([]types.Hash, error) - - // GetFilterLogs performs eth_getFilterLogs RPC call. - // - // It returns an array of all logs matching filter with given ID. - GetFilterLogs(ctx context.Context, id *big.Int) ([]types.Log, error) - - // GetLogs performs eth_getLogs RPC call. - // - // It returns logs that match the given query. - GetLogs(ctx context.Context, query *types.FilterLogsQuery) ([]types.Log, error) - - // MaxPriorityFeePerGas performs eth_maxPriorityFeePerGas RPC call. - // - // It returns the estimated maximum priority fee per gas. - MaxPriorityFeePerGas(ctx context.Context) (*big.Int, error) - - // SubscribeLogs performs eth_subscribe RPC call with "logs" subscription - // type. - // - // It creates a subscription that will send logs that match the given query. - // - // Subscription channel will be closed when the context is canceled. - SubscribeLogs(ctx context.Context, query *types.FilterLogsQuery) (<-chan types.Log, error) - - // SubscribeNewHeads performs eth_subscribe RPC call with "newHeads" - // subscription type. - // - // It creates a subscription that will send new block headers. - // - // Subscription channel will be closed when the context is canceled. - SubscribeNewHeads(ctx context.Context) (<-chan types.Block, error) - - // SubscribeNewPendingTransactions performs eth_subscribe RPC call with - // "newPendingTransactions" subscription type. - // - // It creates a subscription that will send new pending transactions. - // - // Subscription channel will be closed when the context is canceled. - SubscribeNewPendingTransactions(ctx context.Context) (<-chan types.Hash, error) -} diff --git a/rpc/transport/combined.go b/rpc/transport/combined.go index e5aa031..fbcdd26 100644 --- a/rpc/transport/combined.go +++ b/rpc/transport/combined.go @@ -5,17 +5,17 @@ import ( "encoding/json" ) -// Combined is transport that uses separate transports for regular calls and +// Combined is a transport that uses separate transports for regular calls and // subscriptions. // -// It is recommended by some RPC providers to use HTTP for regular calls and -// WebSockets for subscriptions. +// Some RPC providers recommend using HTTP for regular calls and WebSockets for +// subscriptions. type Combined struct { calls Transport subs SubscriptionTransport } -// NewCombined creates a new Combined transport. +// NewCombined creates a new [Combined] instance. func NewCombined(call Transport, subscriber SubscriptionTransport) *Combined { return &Combined{ calls: call, @@ -23,17 +23,17 @@ func NewCombined(call Transport, subscriber SubscriptionTransport) *Combined { } } -// Call implements the Transport interface. +// Call implements the [Transport] interface. func (c *Combined) Call(ctx context.Context, result any, method string, args ...any) error { return c.calls.Call(ctx, result, method, args...) } -// Subscribe implements the SubscriptionTransport interface. +// Subscribe implements the [SubscriptionTransport] interface. func (c *Combined) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { return c.subs.Subscribe(ctx, method, args...) } -// Unsubscribe implements the SubscriptionTransport interface. +// Unsubscribe implements the [SubscriptionTransport] interface. func (c *Combined) Unsubscribe(ctx context.Context, id string) error { return c.subs.Unsubscribe(ctx, id) } diff --git a/rpc/transport/error_test.go b/rpc/transport/error_test.go deleted file mode 100644 index 339d7ce..0000000 --- a/rpc/transport/error_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package transport - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/defiweb/go-eth/hexutil" -) - -func TestNewRPCError(t *testing.T) { - tests := []struct { - name string - code int - message string - data any - expected *RPCError - }{ - { - name: "error with non-hex data", - code: ErrCodeGeneral, - message: "Unauthorized access", - data: "some data", - expected: &RPCError{ - Code: ErrCodeGeneral, - Message: "Unauthorized access", - Data: "some data", - }, - }, - { - name: "error with hex data", - code: ErrCodeGeneral, - message: "Invalid request", - data: "0x68656c6c6f", - expected: &RPCError{ - Code: ErrCodeGeneral, - Message: "Invalid request", - Data: hexutil.MustHexToBytes("0x68656c6c6f"), - }, - }, - // Add more test cases as needed - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - actual := NewRPCError(tt.code, tt.message, tt.data) - assert.Equal(t, tt.expected, actual) - }) - } -} diff --git a/rpc/transport/hijack.go b/rpc/transport/hijack.go new file mode 100644 index 0000000..9f2b982 --- /dev/null +++ b/rpc/transport/hijack.go @@ -0,0 +1,152 @@ +package transport + +import ( + "context" + "encoding/json" +) + +// Hijacker intercepts and modifies calls to the underlying [Transport] using +// the middleware pattern. +// +// The 'next' function should be called to continue the call chain. +// +// The transport provided to 'next' is the underlying [Transport] instance; +// using it will bypass any subsequent hijackers. +type Hijacker interface { + // Call returns a [CallFunc] that intercepts and modifies + // the 'Call' method. + // + // If nil is returned, no middleware is applied. + Call() func(next CallFunc) CallFunc + + // Subscribe returns a [SubscribeFunc] that intercepts and modifies + // the 'Subscribe' method. + // + // If nil is returned, no middleware is applied. + Subscribe() func(next SubscribeFunc) SubscribeFunc + + // Unsubscribe returns an [UnsubscribeFunc that intercepts and modifies + // the 'Unsubscribe' method. + // + // If nil is returned, no middleware is applied. + Unsubscribe() func(next UnsubscribeFunc) UnsubscribeFunc +} + +type ( + CallFunc func(ctx context.Context, t Transport, result any, method string, args ...any) error + SubscribeFunc func(ctx context.Context, t SubscriptionTransport, method string, args ...any) (ch chan json.RawMessage, id string, err error) + UnsubscribeFunc func(ctx context.Context, t SubscriptionTransport, id string) error +) + +// Hijack is a wrapper around another [Transport] that allows hijacking +// and modifying the behavior of the underlying [Transport] using the +// middleware pattern. +// +// Hijackers must implement one or more of the Hijacker interface methods. +type Hijack struct { + transport Transport + callFunc CallFunc + subFunc SubscribeFunc + unsubFunc UnsubscribeFunc +} + +// NewHijacker creates a new [Hijack] instance. +func NewHijacker(t Transport, hs ...Hijacker) *Hijack { + h := &Hijack{ + transport: t, + callFunc: defCall, + subFunc: defSub, + unsubFunc: defUnsub, + } + h.Use(hs...) + return h +} + +// Use adds hijackers in the order they are provided. +func (h *Hijack) Use(hs ...Hijacker) { + for _, m := range hs { + if m == nil { + continue + } + if call := m.Call(); call != nil { + h.callFunc = call(h.callFunc) + } + if sub := m.Subscribe(); sub != nil { + h.subFunc = sub(h.subFunc) + } + if unsub := m.Unsubscribe(); unsub != nil { + h.unsubFunc = unsub(h.unsubFunc) + } + } +} + +// Call implements the [Transport] interface. +func (h *Hijack) Call(ctx context.Context, result any, method string, args ...any) error { + hs := getHijackers(ctx) + fn := h.callFunc + for i := 0; i < len(hs); i++ { + if call := hs[i].Call(); call != nil { + fn = call(fn) + } + } + return fn(ctx, h.transport, result, method, args...) +} + +// Subscribe implements the [SubscriptionTransport] interface. +func (h *Hijack) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { + if s, ok := h.transport.(SubscriptionTransport); ok { + hs := getHijackers(ctx) + fn := h.subFunc + for i := 0; i < len(hs); i++ { + if sub := hs[i].Subscribe(); sub != nil { + fn = sub(fn) + } + } + return fn(ctx, s, method, args...) + } + return nil, "", ErrNotSubscriptionTransport +} + +// Unsubscribe implements the [SubscriptionTransport] interface. +func (h *Hijack) Unsubscribe(ctx context.Context, id string) error { + if s, ok := h.transport.(SubscriptionTransport); ok { + hs := getHijackers(ctx) + fn := h.unsubFunc + for i := 0; i < len(hs); i++ { + if unsub := hs[i].Unsubscribe(); unsub != nil { + fn = unsub(fn) + } + } + return fn(ctx, s, id) + } + return ErrNotSubscriptionTransport +} + +// WithHijackers returns a new context with the provided hijackers added to it. +// +// This allows you to pass hijackers down the call chain. The provided hijackers +// will be appended to any existing hijackers in the context. +func WithHijackers(ctx context.Context, hs ...Hijacker) context.Context { + return context.WithValue(ctx, hijackerContextKey{}, append(getHijackers(ctx), hs...)) +} + +func getHijackers(ctx context.Context) []Hijacker { + if hs, ok := ctx.Value(hijackerContextKey{}).([]Hijacker); ok { + return hs + } + return nil +} + +func defCall(ctx context.Context, t Transport, result any, method string, args ...any) error { + return t.Call(ctx, result, method, args...) +} + +func defSub(ctx context.Context, t SubscriptionTransport, method string, args ...any) (ch chan json.RawMessage, id string, err error) { + return t.Subscribe(ctx, method, args...) +} + +func defUnsub(ctx context.Context, t SubscriptionTransport, id string) error { + return t.Unsubscribe(ctx, id) +} + +type hijackerContextKey struct{} diff --git a/rpc/transport/hijack_test.go b/rpc/transport/hijack_test.go new file mode 100644 index 0000000..eda42b5 --- /dev/null +++ b/rpc/transport/hijack_test.go @@ -0,0 +1,169 @@ +package transport + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHijacker(t *testing.T) { + tests := []struct { + transport *mockTransport + asserts func(t *testing.T, f *mockTransport, h *Hijack) + }{ + // Test with no hijackers. + { + transport: newMockTransport(), + asserts: func(t *testing.T, f *mockTransport, h *Hijack) { + go func() { + f.callResult <- nil + f.subResult <- nil + f.unsubResult <- nil + }() + err := h.Call(context.Background(), nil, "foo") + require.NoError(t, err) + + _, _, err = h.Subscribe(context.Background(), "bar") + require.NoError(t, err) + + err = h.Unsubscribe(context.Background(), "baz") + require.NoError(t, err) + + require.Equal(t, 1, f.callCount) + require.Equal(t, 1, f.subCount) + require.Equal(t, 1, f.unsubCount) + }, + }, + // Test the order of hijackers. + { + transport: newMockTransport(), + asserts: func(t *testing.T, f *mockTransport, h *Hijack) { + var order []string + + h.Use(&mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call1") + return next(ctx, t, result, method, args...) + } + }}) + + h.Use(&mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call2") + return next(ctx, t, result, method, args...) + } + }}) + + go func() { + f.callResult <- nil + }() + err := h.Call(context.Background(), nil, "foo") + require.NoError(t, err) + + assert.Equal(t, []string{"call2", "call1"}, order) + assert.Equal(t, 1, f.callCount) + }, + }, + // Test the order of context hijackers. + { + transport: newMockTransport(), + asserts: func(t *testing.T, f *mockTransport, h *Hijack) { + var order []string + + ctx := WithHijackers(context.Background(), &mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call1") + return next(ctx, t, result, method, args...) + } + }}) + + ctx = WithHijackers(ctx, &mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call2") + return next(ctx, t, result, method, args...) + } + }}) + + go func() { + f.callResult <- nil + }() + err := h.Call(ctx, nil, "foo") + require.NoError(t, err) + + assert.Equal(t, []string{"call2", "call1"}, order) + assert.Equal(t, 1, f.callCount) + }, + }, + // Test the order of hijackers in context mixed with the "Use" method. + { + transport: newMockTransport(), + asserts: func(t *testing.T, f *mockTransport, h *Hijack) { + var order []string + + h.Use(&mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call1") + return next(ctx, t, result, method, args...) + } + }}) + + h.Use(&mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call2") + return next(ctx, t, result, method, args...) + } + }}) + + ctx := WithHijackers(context.Background(), &mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call3") + return next(ctx, t, result, method, args...) + } + }}) + + ctx = WithHijackers(ctx, &mockHijacker{callFn: func(next CallFunc) CallFunc { + return func(ctx context.Context, t Transport, result any, method string, args ...any) (err error) { + order = append(order, "call4") + return next(ctx, t, result, method, args...) + } + }}) + + go func() { + f.callResult <- nil + }() + err := h.Call(ctx, nil, "foo") + require.NoError(t, err) + + assert.Equal(t, []string{"call4", "call3", "call2", "call1"}, order) + assert.Equal(t, 1, f.callCount) + }, + }, + } + for n, test := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + h := NewHijacker(test.transport) + test.asserts(t, test.transport, h) + }) + } +} + +type mockHijacker struct { + callFn func(next CallFunc) CallFunc + subFn func(next SubscribeFunc) SubscribeFunc + unsubFn func(next UnsubscribeFunc) UnsubscribeFunc +} + +func (m *mockHijacker) Call() func(next CallFunc) CallFunc { + return m.callFn +} + +func (m *mockHijacker) Subscribe() func(next SubscribeFunc) SubscribeFunc { + return m.subFn +} + +func (m *mockHijacker) Unsubscribe() func(next UnsubscribeFunc) UnsubscribeFunc { + return m.unsubFn +} diff --git a/rpc/transport/http.go b/rpc/transport/http.go index 413c84e..df2dc33 100644 --- a/rpc/transport/http.go +++ b/rpc/transport/http.go @@ -10,13 +10,13 @@ import ( "sync/atomic" ) -// HTTP is a Transport implementation that uses the HTTP protocol. +// HTTP is a [Transport] implementation that uses the HTTP protocol. type HTTP struct { opts HTTPOptions id uint64 } -// HTTPOptions contains options for the HTTP transport. +// HTTPOptions contains options for the [HTTP] transport. type HTTPOptions struct { // URL of the HTTP endpoint. URL string @@ -29,7 +29,7 @@ type HTTPOptions struct { HTTPHeader http.Header } -// NewHTTP creates a new HTTP instance. +// NewHTTP creates a new [HTTP] instance. func NewHTTP(opts HTTPOptions) (*HTTP, error) { if opts.URL == "" { return nil, errors.New("URL cannot be empty") @@ -40,7 +40,7 @@ func NewHTTP(opts HTTPOptions) (*HTTP, error) { return &HTTP{opts: opts}, nil } -// Call implements the Transport interface. +// Call implements the [Transport] interface. func (h *HTTP) Call(ctx context.Context, result any, method string, args ...any) error { id := atomic.AddUint64(&h.id, 1) rpcReq, err := newRPCRequest(&id, method, args) @@ -68,6 +68,9 @@ func (h *HTTP) Call(ctx context.Context, result any, method string, args ...any) if err := json.NewDecoder(httpRes.Body).Decode(rpcRes); err != nil { // If the response is not a valid JSON-RPC response, return the HTTP // status code as the error code. + if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 { + return fmt.Errorf("failed to unmarshal RPC response: %w", err) + } return NewHTTPError(httpRes.StatusCode, nil) } if rpcRes.Error != nil { diff --git a/rpc/transport/http_test.go b/rpc/transport/http_test.go index 472680f..cb55daa 100644 --- a/rpc/transport/http_test.go +++ b/rpc/transport/http_test.go @@ -28,7 +28,7 @@ type httpMock struct { //nolint:funlen func TestHTTP(t *testing.T) { - tests := []struct { + tc := []struct { asserts func(t *testing.T, h *httpMock) }{ // Simple request: @@ -126,7 +126,7 @@ func TestHTTP(t *testing.T) { }, }, } - for n, tt := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { h := &httpMock{} h.HTTP, _ = NewHTTP(HTTPOptions{ diff --git a/rpc/transport/ipc.go b/rpc/transport/ipc.go index 5f460e9..1b99568 100644 --- a/rpc/transport/ipc.go +++ b/rpc/transport/ipc.go @@ -10,13 +10,13 @@ import ( "time" ) -// IPC is a Transport implementation that uses the IPC protocol. +// IPC is a [Transport] implementation that uses the IPC protocol. type IPC struct { *stream conn net.Conn } -// IPCOptions contains options for the IPC transport. +// IPCOptions contains options for the [IPC] transport. type IPCOptions struct { // Context used to close the connection. Context context.Context @@ -31,7 +31,7 @@ type IPCOptions struct { ErrorCh chan error } -// NewIPC creates a new IPC instance. +// NewIPC creates a new [IPC] instance. func NewIPC(opts IPCOptions) (*IPC, error) { var d net.Dialer conn, err := d.DialContext(opts.Context, "unix", opts.Path) diff --git a/rpc/transport/retry.go b/rpc/transport/retry.go index a85c5e9..3ff6306 100644 --- a/rpc/transport/retry.go +++ b/rpc/transport/retry.go @@ -12,13 +12,14 @@ import ( var ErrNotSubscriptionTransport = errors.New("transport does not implement SubscriptionTransport") var ( - // RetryOnAnyError retries on any error except for the following: + // RetryOnAnyError retries on any error except for the following cases, + // where retrying does not make sense: // 3: Execution error. // -32700: Parse error. // -32600: Invalid request. // -32601: Method not found. // -32602: Invalid params. - // -32000: If error message starts with "execution reverted". + // -32000: If the error message starts with "execution reverted". RetryOnAnyError = func(err error) bool { // List of errors that should not be retried: switch errorCode(err) { @@ -33,7 +34,7 @@ var ( case ErrCodeInvalidParams: return false case ErrCodeGeneral: - rpcErr := &RPCError{} + var rpcErr *RPCError if errors.As(err, &rpcErr) { if strings.HasPrefix(rpcErr.Message, "execution reverted") { return false @@ -45,10 +46,10 @@ var ( return err != nil } - // RetryOnLimitExceeded retries on the following errors: + // RetryOnLimitExceeded retries only on the following errors: // -32005: Limit exceeded. // -32097: Rate limit reached (Blast). - // 429: Too many requests + // 429: Too many requests. RetryOnLimitExceeded = func(err error) bool { switch errorCode(err) { case ErrCodeLimitExceeded: @@ -62,7 +63,8 @@ var ( } ) -// ExponentialBackoffOptions contains options for the ExponentialBackoff function. +// ExponentialBackoffOptions contains options for the [ExponentialBackoff] +// function. type ExponentialBackoffOptions struct { // BaseDelay is the base delay before the first retry. BaseDelay time.Duration @@ -70,20 +72,23 @@ type ExponentialBackoffOptions struct { // MaxDelay is the maximum delay between retries. MaxDelay time.Duration - // ExponentialFactor is the exponential factor to use for calculating the delay. + // ExponentialFactor is the exponential factor to use for calculating the + // delay. + // // The delay is calculated as BaseDelay * ExponentialFactor ^ retryCount. ExponentialFactor float64 } var ( - // LinearBackoff returns a BackoffFunc that returns a constant delay. + // LinearBackoff returns a [BackoffFunc] that returns a constant delay. LinearBackoff = func(delay time.Duration) func(int) time.Duration { return func(_ int) time.Duration { return delay } } - // ExponentialBackoff returns a BackoffFunc that returns an exponential delay. + // ExponentialBackoff returns a [BackoffFunc] that returns an exponential delay. + // // The delay is calculated as BaseDelay * ExponentialFactor ^ retryCount. ExponentialBackoff = func(opts ExponentialBackoffOptions) func(int) time.Duration { return func(retryCount int) time.Duration { @@ -96,30 +101,31 @@ var ( } ) -// Retry is a wrapper around another transport that retries requests. +// Retry wraps another [Transport] and retries requests on failure. type Retry struct { opts RetryOptions } -// RetryOptions contains options for the Retry transport. +// RetryOptions contains options for the [Retry] transport. type RetryOptions struct { // Transport is the underlying transport to use. Transport Transport // RetryFunc is a function that returns true if the request should be - // retried. The RetryOnAnyError and RetryOnLimitExceeded functions can be - // used or a custom function can be provided. + // retried. You can use [RetryOnAnyError], [RetryOnLimitExceeded], or + // provide a custom function. RetryFunc func(error) bool // BackoffFunc is a function that returns the delay before the next retry. // It takes the current retry count as an argument. BackoffFunc func(int) time.Duration - // MaxRetries is the maximum number of retries. If negative, there is no limit. + // MaxRetries is the maximum number of retries. + // If negative, there is no limit. MaxRetries int } -// NewRetry creates a new Retry instance. +// NewRetry creates a new [Retry] instance. func NewRetry(opts RetryOptions) (*Retry, error) { if opts.Transport == nil { return nil, errors.New("transport cannot be nil") @@ -136,7 +142,7 @@ func NewRetry(opts RetryOptions) (*Retry, error) { return &Retry{opts: opts}, nil } -// Call implements the Transport interface. +// Call implements the [Transport] interface. func (c *Retry) Call(ctx context.Context, result any, method string, args ...any) (err error) { var i int for { @@ -157,7 +163,7 @@ func (c *Retry) Call(ctx context.Context, result any, method string, args ...any return err } -// Subscribe implements the SubscriptionTransport interface. +// Subscribe implements the [SubscriptionTransport] interface. func (c *Retry) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { if s, ok := c.opts.Transport.(SubscriptionTransport); ok { var i int @@ -181,7 +187,7 @@ func (c *Retry) Subscribe(ctx context.Context, method string, args ...any) (ch c return nil, "", ErrNotSubscriptionTransport } -// Unsubscribe implements the SubscriptionTransport interface. +// Unsubscribe implements the [SubscriptionTransport] interface. func (c *Retry) Unsubscribe(ctx context.Context, id string) (err error) { if s, ok := c.opts.Transport.(SubscriptionTransport); ok { var i int diff --git a/rpc/transport/retry_test.go b/rpc/transport/retry_test.go index a8104df..95693aa 100644 --- a/rpc/transport/retry_test.go +++ b/rpc/transport/retry_test.go @@ -2,7 +2,6 @@ package transport import ( "context" - "encoding/json" "fmt" "testing" "time" @@ -10,54 +9,21 @@ import ( "github.com/stretchr/testify/require" ) -type fakeTransport struct { - callResult chan error - subResult chan error - unsubResult chan error - callCount int - subCount int - unsubCount int -} - -func newFakeTransport() *fakeTransport { - return &fakeTransport{ - callResult: make(chan error), - subResult: make(chan error), - unsubResult: make(chan error), - } -} - -func (f *fakeTransport) Call(ctx context.Context, result any, method string, args ...any) error { - f.callCount++ - return <-f.callResult -} - -func (f *fakeTransport) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { - f.subCount++ - err = <-f.subResult - return nil, "", err -} - -func (f *fakeTransport) Unsubscribe(ctx context.Context, id string) error { - f.unsubCount++ - return <-f.unsubResult -} - //nolint:funlen func TestRetry(t *testing.T) { - tests := []struct { + tc := []struct { retry RetryOptions - asserts func(t *testing.T, f *fakeTransport, r *Retry) + asserts func(t *testing.T, f *mockTransport, r *Retry) }{ // No retry on success (call). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.callResult <- nil }() @@ -71,12 +37,12 @@ func TestRetry(t *testing.T) { // No retry on success (subscribe). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.subResult <- nil }() @@ -90,12 +56,12 @@ func TestRetry(t *testing.T) { // No retry on success (unsubscribe). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.unsubResult <- nil }() @@ -109,12 +75,12 @@ func TestRetry(t *testing.T) { // Retry on error (call). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.callResult <- fmt.Errorf("foo") f.callResult <- nil @@ -129,12 +95,12 @@ func TestRetry(t *testing.T) { // Retry on error (subscribe). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.subResult <- fmt.Errorf("foo") f.subResult <- nil @@ -149,12 +115,12 @@ func TestRetry(t *testing.T) { // Retry on error (unsubscribe). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.unsubResult <- fmt.Errorf("foo") f.unsubResult <- nil @@ -169,12 +135,12 @@ func TestRetry(t *testing.T) { // Too many retries (call). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.callResult <- fmt.Errorf("foo") f.callResult <- fmt.Errorf("foo") @@ -189,12 +155,12 @@ func TestRetry(t *testing.T) { // Too many retries (subscribe). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.subResult <- fmt.Errorf("foo") f.subResult <- fmt.Errorf("foo") @@ -209,12 +175,12 @@ func TestRetry(t *testing.T) { // Too many retries (unsubscribe). { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.unsubResult <- fmt.Errorf("foo") f.unsubResult <- fmt.Errorf("foo") @@ -229,12 +195,12 @@ func TestRetry(t *testing.T) { // Infinite retries until success { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: -1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.callResult <- fmt.Errorf("foo") f.callResult <- fmt.Errorf("foo") @@ -272,12 +238,12 @@ func TestRetry(t *testing.T) { // Infinite retries until context is canceled. { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: -1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { ctx, cancel := context.WithCancel(context.Background()) go func() { f.callResult <- fmt.Errorf("foo") @@ -306,12 +272,12 @@ func TestRetry(t *testing.T) { // Do not retry if RetryFunc returns false. { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: func(error) bool { return false }, BackoffFunc: LinearBackoff(0), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { go func() { f.callResult <- fmt.Errorf("foo") f.subResult <- fmt.Errorf("foo") @@ -334,12 +300,12 @@ func TestRetry(t *testing.T) { // Do not wait for backoff after the last retry. { retry: RetryOptions{ - Transport: newFakeTransport(), + Transport: newMockTransport(), MaxRetries: 1, RetryFunc: RetryOnAnyError, BackoffFunc: LinearBackoff(100 * time.Millisecond), }, - asserts: func(t *testing.T, f *fakeTransport, r *Retry) { + asserts: func(t *testing.T, f *mockTransport, r *Retry) { t0 := time.Now() go func() { f.callResult <- fmt.Errorf("foo") @@ -368,18 +334,18 @@ func TestRetry(t *testing.T) { }, }, } - for n, test := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - r, err := NewRetry(test.retry) + r, err := NewRetry(tt.retry) require.NoError(t, err) - test.asserts(t, r.opts.Transport.(*fakeTransport), r) + tt.asserts(t, r.opts.Transport.(*mockTransport), r) }) } } //nolint:dupl func TestRetryOnAnyError(t *testing.T) { - tests := []struct { + tc := []struct { err error want bool }{ @@ -444,17 +410,17 @@ func TestRetryOnAnyError(t *testing.T) { want: false, }, } - for n, test := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - got := RetryOnAnyError(test.err) - require.Equal(t, test.want, got) + got := RetryOnAnyError(tt.err) + require.Equal(t, tt.want, got) }) } } //nolint:dupl func TestRetryOnLimitExceeded(t *testing.T) { - tests := []struct { + tc := []struct { err error want bool }{ @@ -511,16 +477,16 @@ func TestRetryOnLimitExceeded(t *testing.T) { want: true, }, } - for n, test := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - got := RetryOnLimitExceeded(test.err) - require.Equal(t, test.want, got) + got := RetryOnLimitExceeded(tt.err) + require.Equal(t, tt.want, got) }) } } func TestLinearBackoff(t *testing.T) { - tests := []struct { + tc := []struct { delay time.Duration want []time.Duration }{ @@ -539,10 +505,10 @@ func TestLinearBackoff(t *testing.T) { }, }, } - for n, test := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - b := LinearBackoff(test.delay) - for i, want := range test.want { + b := LinearBackoff(tt.delay) + for i, want := range tt.want { got := b(i) require.Equal(t, want, got) } @@ -551,7 +517,7 @@ func TestLinearBackoff(t *testing.T) { } func TestExponentialBackoff(t *testing.T) { - tests := []struct { + tc := []struct { opts ExponentialBackoffOptions want []time.Duration }{ @@ -582,10 +548,10 @@ func TestExponentialBackoff(t *testing.T) { }, }, } - for n, test := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - b := ExponentialBackoff(test.opts) - for i, want := range test.want { + b := ExponentialBackoff(tt.opts) + for i, want := range tt.want { got := b(i) require.Equal(t, want, got) } diff --git a/rpc/transport/stream.go b/rpc/transport/stream.go index 79d67c6..165b7d3 100644 --- a/rpc/transport/stream.go +++ b/rpc/transport/stream.go @@ -30,7 +30,7 @@ type stream struct { } // initStream initializes the stream struct with default values and starts -// goroutines. +// the required goroutines. func (s *stream) initStream() *stream { s.writerCh = make(chan rpcRequest) s.readerCh = make(chan rpcResponse) @@ -41,7 +41,7 @@ func (s *stream) initStream() *stream { return s } -// Call implements the Transport interface. +// Call implements the [Transport] interface. func (s *stream) Call(ctx context.Context, result any, method string, args ...any) error { ctx, ctxCancel := context.WithTimeout(ctx, s.timeout) defer ctxCancel() @@ -62,7 +62,7 @@ func (s *stream) Call(ctx context.Context, result any, method string, args ...an s.writerCh <- req // Wait for the response. - // The response is handled by the streamRoutine. It will send the response + // The response is handled by streamRoutine, which sends the response // to the ch channel. select { case res := <-ch: @@ -84,7 +84,7 @@ func (s *stream) Call(ctx context.Context, result any, method string, args ...an return nil } -// Subscribe implements the SubscriptionTransport interface. +// Subscribe implements the [SubscriptionTransport] interface. func (s *stream) Subscribe(ctx context.Context, method string, args ...any) (chan json.RawMessage, string, error) { rawID := types.Number{} params := make([]any, 0, 2) @@ -101,7 +101,7 @@ func (s *stream) Subscribe(ctx context.Context, method string, args ...any) (cha return ch, id, nil } -// Unsubscribe implements the SubscriptionTransport interface. +// Unsubscribe implements the [SubscriptionTransport] interface. func (s *stream) Unsubscribe(ctx context.Context, id string) error { if !s.delSubCh(id) { return errors.New("unknown subscription") @@ -113,8 +113,8 @@ func (s *stream) Unsubscribe(ctx context.Context, id string) error { return s.Call(ctx, nil, "eth_unsubscribe", num) } -// readerRoutine reads messages from the stream connection and dispatches -// them to the appropriate channel. +// streamRoutine reads messages from the stream connection and dispatches +// them to the appropriate channels. func (s *stream) streamRoutine() { for { res, ok := <-s.readerCh @@ -157,17 +157,17 @@ func (s *stream) contextHandlerRoutine() { } } -// addCallCh adds a channel to the calls map. Incoming response that match the -// id will be sent to the given channel. Because message ids are unique, the -// channel must be deleted after the response is received using delCallCh. +// addCallCh adds a channel to the calls map. Incoming responses that match +// the id will be sent to the given channel. Because message ids are unique, +// the channel must be deleted after the response is received using delCallCh. func (s *stream) addCallCh(id uint64, ch chan rpcResponse) { s.mu.Lock() defer s.mu.Unlock() s.calls[id] = ch } -// addSubCh adds a channel to the subs map. Incoming subscription notifications -// that match the id will be sent to the given channel. +// addSubCh adds a channel to the subs map. Incoming subscription +// notifications that match the id will be sent to the given channel. func (s *stream) addSubCh(id string, ch chan json.RawMessage) { s.mu.Lock() defer s.mu.Unlock() @@ -207,8 +207,8 @@ func (s *stream) callChSend(id uint64, res rpcResponse) { } } -// subChSend sends a subscription notification to the channel that matches the -// id. +// subChSend sends a subscription notification to the channel that matches +// the id. func (s *stream) subChSend(id string, res json.RawMessage) { s.mu.RLock() defer s.mu.RUnlock() diff --git a/rpc/transport/transport.go b/rpc/transport/transport.go index ff49829..8fe6752 100644 --- a/rpc/transport/transport.go +++ b/rpc/transport/transport.go @@ -1,3 +1,5 @@ +// Package transport provides transport layer implementations for the Ethereum +// JSON-RPC API. package transport import ( @@ -13,7 +15,7 @@ type Transport interface { Call(ctx context.Context, result any, method string, args ...any) error } -// SubscriptionTransport is transport that supports subscriptions. +// SubscriptionTransport is a transport that supports subscriptions. type SubscriptionTransport interface { Transport @@ -26,12 +28,12 @@ type SubscriptionTransport interface { Unsubscribe(ctx context.Context, id string) error } -// New returns a new Transport instance based on the URL scheme. -// Supported schemes are: http, https, ws, wss. -// If scheme is empty, it will use IPC. +// New returns a new [Transport] instance based on the URL scheme. +// Supported schemes are: http, https, ws, and wss. +// If the scheme is empty, IPC will be used. // // The context is used to close the underlying connection when the transport -// uses a websocket or IPC. +// uses a WebSocket or IPC. func New(ctx context.Context, rpcURL string) (Transport, error) { url, err := netURL.Parse(rpcURL) if err != nil { diff --git a/rpc/transport/transport_test.go b/rpc/transport/transport_test.go new file mode 100644 index 0000000..5f3d929 --- /dev/null +++ b/rpc/transport/transport_test.go @@ -0,0 +1,39 @@ +package transport + +import ( + "context" + "encoding/json" +) + +type mockTransport struct { + callResult chan error + subResult chan error + unsubResult chan error + callCount int + subCount int + unsubCount int +} + +func newMockTransport() *mockTransport { + return &mockTransport{ + callResult: make(chan error), + subResult: make(chan error), + unsubResult: make(chan error), + } +} + +func (t *mockTransport) Call(ctx context.Context, result any, method string, args ...any) error { + t.callCount++ + return <-t.callResult +} + +func (t *mockTransport) Subscribe(ctx context.Context, method string, args ...any) (ch chan json.RawMessage, id string, err error) { + t.subCount++ + err = <-t.subResult + return nil, "", err +} + +func (t *mockTransport) Unsubscribe(ctx context.Context, id string) error { + t.unsubCount++ + return <-t.unsubResult +} diff --git a/rpc/transport/websocket.go b/rpc/transport/websocket.go index 1820bc6..bf27465 100644 --- a/rpc/transport/websocket.go +++ b/rpc/transport/websocket.go @@ -11,37 +11,35 @@ import ( "nhooyr.io/websocket/wsjson" ) -// Websocket is a Transport implementation that uses the websocket -// protocol. +// Websocket is a [Transport] implementation that uses the WebSocket protocol. type Websocket struct { *stream conn *websocket.Conn } -// WebsocketOptions contains options for the websocket transport. +// WebsocketOptions contains options for the [WebSocket] transport. type WebsocketOptions struct { - // Context used to close the connection. + // Context is used to close the connection. Context context.Context - // URL of the websocket endpoint. + // URL is the WebSocket endpoint. URL string - // HTTPClient is the HTTP client to use. If nil, http.DefaultClient is - // used. + // HTTPClient is the HTTP client to use. If nil, http.DefaultClient is used. HTTPClient *http.Client - // HTTPHeader specifies the HTTP headers to be included in the - // websocket handshake request. + // HTTPHeader specifies the HTTP headers to include in the WebSocket + // handshake request. HTTPHeader http.Header - // Timeout is the timeout for the websocket requests. Default is 60s. + // Timeout is the timeout for WebSocket requests. The default is 60s. Timout time.Duration // ErrorCh is an optional channel used to report errors. ErrorCh chan error } -// NewWebsocket creates a new Websocket instance. +// NewWebsocket creates a new [Websocket] instance. func NewWebsocket(opts WebsocketOptions) (*Websocket, error) { if opts.URL == "" { return nil, errors.New("URL cannot be empty") @@ -75,9 +73,9 @@ func NewWebsocket(opts WebsocketOptions) (*Websocket, error) { } func (ws *Websocket) readerRoutine() { - // The background context is used here because closing context will - // cause the nhooyr.io/websocket package to close a connection with - // a close code of 1008 (policy violation) which is not what we want. + // The background context is used here because closing the context will + // cause the nhooyr.io/websocket package to close the connection with a + // close code of 1008 (policy violation), which is not what we want. ctx := context.Background() for { res := rpcResponse{} diff --git a/rpc/transport/websocket_test.go b/rpc/transport/websocket_test.go index 288d54d..92618d0 100644 --- a/rpc/transport/websocket_test.go +++ b/rpc/transport/websocket_test.go @@ -21,7 +21,7 @@ import ( //nolint:funlen func TestWebsocket(t *testing.T) { - tests := []struct { + tc := []struct { asserts func(t *testing.T, ws *Websocket, reqCh, resCh chan string) }{ // Simple case: @@ -121,7 +121,7 @@ func TestWebsocket(t *testing.T) { }, }, } - for n, tt := range tests { + for n, tt := range tc { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { wg := sync.WaitGroup{} reqCh := make(chan string) // Received requests. diff --git a/rpc/util.go b/rpc/util.go index 5bade5c..d719aef 100644 --- a/rpc/util.go +++ b/rpc/util.go @@ -1,32 +1,193 @@ package rpc import ( + "context" "encoding/json" + "errors" + "github.com/defiweb/go-eth/rpc/transport" "github.com/defiweb/go-eth/types" ) +// getTransactionData extracts the [types.TransactionData] from the given +// value. +func getTransactionData(v any) *types.TransactionData { + if td, ok := v.(types.HasTransactionData); ok { + return td.GetTransactionData() + } + return nil +} + +// getCallData extracts the [types.CallData] from the given value. +func getCallData(v any) *types.CallData { + if cd, ok := v.(types.HasCallData); ok { + return cd.GetCallData() + } + return nil +} + +// getLegacyPriceData extracts the [types.LegacyPriceData] from the given +// value. +func getLegacyPriceData(v any) *types.LegacyPriceData { + if lpc, ok := v.(types.HasLegacyPriceData); ok { + return lpc.GetLegacyPriceData() + } + return nil +} + +// getAccessListData extracts the [types.AccessListData] from the given value. +func getAccessListData(v any) *types.AccessListData { + if ald, ok := v.(types.HasAccessListData); ok { + return ald.GetAccessListData() + } + return nil +} + +// getDynamicFeeData extracts the [types.DynamicFeeData] from the given value. +func getDynamicFeeData(v any) *types.DynamicFeeData { + if dfd, ok := v.(types.HasDynamicFeeData); ok { + return dfd.GetDynamicFeeData() + } + return nil +} + +// convertTXToLegacyPrice converts a transaction to one that has legacy +// price data. +func convertTXToLegacyPrice(tx types.Transaction) types.Transaction { + if getLegacyPriceData(tx) != nil { + return tx + } + typ := types.LegacyTxType + if getAccessListData(tx) != nil { + typ = types.AccessListTxType + } + return convertTX(tx, typ) +} + +// convertTXToAccessList converts a transaction to one that has access list +// data. +func convertTXToDynamicFee(tx types.Transaction) types.Transaction { + if getDynamicFeeData(tx) != nil { + return tx + } + return convertTX(tx, types.DynamicFeeTxType) +} + +// convertTX converts a transaction to the specified type. +func convertTX(tx types.Transaction, typ types.TransactionType) types.Transaction { + if tx.Type() == typ { + return tx + } + switch typ { + case types.LegacyTxType: + ltx := types.NewTransactionLegacy() + ltx.SetTransactionData(*tx.GetTransactionData()) + if tx, ok := tx.(types.HasCallData); ok { + ltx.SetCallData(*tx.GetCallData()) + } + if tx, ok := tx.(types.HasLegacyPriceData); ok { + ltx.SetLegacyPriceData(*tx.GetLegacyPriceData()) + } + return ltx + case types.AccessListTxType: + altx := types.NewTransactionAccessList() + altx.SetTransactionData(*tx.GetTransactionData()) + if tx, ok := tx.(types.HasCallData); ok { + altx.SetCallData(*tx.GetCallData()) + } + if tx, ok := tx.(types.HasLegacyPriceData); ok { + altx.SetLegacyPriceData(*tx.GetLegacyPriceData()) + } + if tx, ok := tx.(types.HasAccessListData); ok { + altx.SetAccessListData(*tx.GetAccessListData()) + } + return altx + case types.DynamicFeeTxType: + dftx := types.NewTransactionDynamicFee() + dftx.SetTransactionData(*tx.GetTransactionData()) + if tx, ok := tx.(types.HasCallData); ok { + dftx.SetCallData(*tx.GetCallData()) + } + if tx, ok := tx.(types.HasAccessListData); ok { + dftx.SetAccessListData(*tx.GetAccessListData()) + } + if tx, ok := tx.(types.HasDynamicFeeData); ok { + dftx.SetDynamicFeeData(*tx.GetDynamicFeeData()) + } + return dftx + case types.BlobTxType: + btx := types.NewTransactionBlob() + btx.SetTransactionData(*tx.GetTransactionData()) + if tx, ok := tx.(types.HasCallData); ok { + btx.SetCallData(*tx.GetCallData()) + } + if tx, ok := tx.(types.HasAccessListData); ok { + btx.SetAccessListData(*tx.GetAccessListData()) + } + if tx, ok := tx.(types.HasDynamicFeeData); ok { + btx.SetDynamicFeeData(*tx.GetDynamicFeeData()) + } + if tx, ok := tx.(types.HasBlobData); ok { + btx.SetBlobData(*tx.GetBlobData()) + } + return btx + default: + return nil + } +} + +// subscribe creates a subscription to the given method and returns a channel +// that will receive the subscription messages. The messages are unmarshalled +// to the T type. The subscription is unsubscribed and channel closed when the +// context is cancelled. +func subscribe[T any](ctx context.Context, t transport.Transport, method string, params ...any) (chan T, error) { + st, ok := t.(transport.SubscriptionTransport) + if !ok { + return nil, errors.New("transport does not support subscriptions") + } + rawCh, subID, err := st.Subscribe(ctx, method, params...) + if err != nil { + return nil, err + } + msgCh := make(chan T) + go func() { + defer close(msgCh) + defer st.Unsubscribe(ctx, subID) + for { + select { + case <-ctx.Done(): + return + case raw, ok := <-rawCh: + if !ok { + return + } + var msg T + if err := json.Unmarshal(raw, &msg); err != nil { + continue + } + msgCh <- msg + } + } + }() + return msgCh, nil +} + // signTransactionResult is the result of an eth_signTransaction request. // Some backends return only RLP encoded data, others return a JSON object, // this type can handle both. type signTransactionResult struct { - Raw types.Bytes `json:"raw"` - Tx *types.Transaction `json:"tx"` + Raw types.Bytes `json:"raw"` } func (s *signTransactionResult) UnmarshalJSON(input []byte) error { + type alias signTransactionResult if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' { return json.Unmarshal(input, &s.Raw) } - type alias struct { - Raw types.Bytes `json:"raw"` - Tx *types.Transaction `json:"tx"` - } var dec alias if err := json.Unmarshal(input, &dec); err != nil { return err } - s.Tx = dec.Tx s.Raw = dec.Raw return nil } diff --git a/txmodifier/chainid.go b/txmodifier/chainid.go deleted file mode 100644 index 9cc9bee..0000000 --- a/txmodifier/chainid.go +++ /dev/null @@ -1,82 +0,0 @@ -package txmodifier - -import ( - "context" - "fmt" - "sync" - - "github.com/defiweb/go-eth/rpc" - "github.com/defiweb/go-eth/types" -) - -// ChainIDProvider is a transaction modifier that sets the chain ID of the -// transaction. -// -// To use this modifier, add it using the WithTXModifiers option when creating -// a new rpc.Client. -type ChainIDProvider struct { - mu sync.Mutex - chainID uint64 - replace bool - cache bool -} - -// ChainIDProviderOptions is the options for NewChainIDProvider. -type ChainIDProviderOptions struct { - // ChainID is the chain ID that will be set for the transaction. - // If 0, the chain ID will be queried from the node. - ChainID uint64 - - // Replace is true if the transaction chain ID should be replaced even if - // it is already set. - Replace bool - - // Cache is true if the chain ID will be cached instead of being queried - // for each transaction. Cached chain ID will be used for all RPC clients - // that use the same ChainIDProvider instance. - // - // If ChainID is set, this option is ignored. - Cache bool -} - -// NewChainIDProvider returns a new ChainIDProvider. -func NewChainIDProvider(opts ChainIDProviderOptions) *ChainIDProvider { - if opts.ChainID != 0 { - opts.Cache = true - } - return &ChainIDProvider{ - chainID: opts.ChainID, - replace: opts.Replace, - cache: opts.Cache, - } -} - -// Modify implements the rpc.TXModifier interface. -func (p *ChainIDProvider) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { - if !p.replace && tx.ChainID != nil { - return nil - } - if !p.cache { - chainID, err := client.ChainID(ctx) - if err != nil { - return fmt.Errorf("chain ID provider: %w", err) - } - tx.ChainID = &chainID - return nil - } - p.mu.Lock() - defer p.mu.Unlock() - var cid uint64 - if p.chainID != 0 { - cid = p.chainID - } else { - chainID, err := client.ChainID(ctx) - if err != nil { - return fmt.Errorf("chain ID provider: %w", err) - } - p.chainID = chainID - cid = chainID - } - tx.ChainID = &cid - return nil -} diff --git a/txmodifier/chainid_test.go b/txmodifier/chainid_test.go deleted file mode 100644 index c67189c..0000000 --- a/txmodifier/chainid_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package txmodifier - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/defiweb/go-eth/types" -) - -func TestChainIDSetter_Modify(t *testing.T) { - ctx := context.Background() - fromAddress := types.MustAddressFromHex("0x1234567890abcdef1234567890abcdef12345678") - - t.Run("cache chain ID", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}} - rpcMock := new(mockRPC) - - provider := NewChainIDProvider(ChainIDProviderOptions{ - ChainID: 1, - }) - _ = provider.Modify(ctx, rpcMock, tx) - - assert.Equal(t, uint64(1), *tx.ChainID) - }) - - t.Run("query RPC node", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}} - rpcMock := new(mockRPC) - rpcMock.On("ChainID", ctx).Return(uint64(1), nil) - - provider := NewChainIDProvider(ChainIDProviderOptions{ - Replace: false, - Cache: false, - }) - err := provider.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, uint64(1), *tx.ChainID) - }) - - t.Run("replace chain ID", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}, ChainID: uint64Ptr(2)} - rpcMock := new(mockRPC) - rpcMock.On("ChainID", ctx).Return(uint64(1), nil) - - provider := NewChainIDProvider(ChainIDProviderOptions{ - Replace: true, - Cache: false, - }) - err := provider.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.NotEqual(t, uint64(2), *tx.ChainID) - }) - - t.Run("do not replace chain ID", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}, ChainID: uint64Ptr(2)} - rpcMock := new(mockRPC) - rpcMock.On("ChainID", ctx).Return(uint64(1), nil) - - provider := NewChainIDProvider(ChainIDProviderOptions{ - Replace: false, - Cache: false, - }) - err := provider.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.NotEqual(t, uint64(1), *tx.ChainID) - }) - - t.Run("cache chain ID", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}, ChainID: uint64Ptr(2)} - rpcMock := new(mockRPC) - rpcMock.On("ChainID", ctx).Return(uint64(1), nil).Once() - - provider := NewChainIDProvider(ChainIDProviderOptions{ - Replace: true, - Cache: true, - }) - _ = provider.Modify(ctx, rpcMock, tx) - _ = provider.Modify(ctx, rpcMock, tx) - }) -} - -func uint64Ptr(i uint64) *uint64 { - return &i -} diff --git a/txmodifier/gasfee.go b/txmodifier/gasfee.go deleted file mode 100644 index 5678a95..0000000 --- a/txmodifier/gasfee.go +++ /dev/null @@ -1,149 +0,0 @@ -package txmodifier - -import ( - "context" - "fmt" - "math/big" - - "github.com/defiweb/go-eth/rpc" - "github.com/defiweb/go-eth/types" -) - -// LegacyGasFeeEstimator is a transaction modifier that estimates gas fee -// using the rpc.GasPrice method. -// -// It sets transaction type to types.LegacyTxType or types.AccessListTxType if -// an access list is provided. -// -// To use this modifier, add it using the WithTXModifiers option when creating -// a new rpc.Client. -type LegacyGasFeeEstimator struct { - multiplier float64 - minGasPrice *big.Int - maxGasPrice *big.Int - replace bool -} - -// LegacyGasFeeEstimatorOptions is the options for NewLegacyGasFeeEstimator. -type LegacyGasFeeEstimatorOptions struct { - Multiplier float64 // Multiplier is applied to the gas price. - MinGasPrice *big.Int // MinGasPrice is the minimum gas price, or nil if there is no lower bound. - MaxGasPrice *big.Int // MaxGasPrice is the maximum gas price, or nil if there is no upper bound. - Replace bool // Replace is true if the gas price should be replaced even if it is already set. -} - -// NewLegacyGasFeeEstimator returns a new LegacyGasFeeEstimator. -func NewLegacyGasFeeEstimator(opts LegacyGasFeeEstimatorOptions) *LegacyGasFeeEstimator { - return &LegacyGasFeeEstimator{ - multiplier: opts.Multiplier, - minGasPrice: opts.MinGasPrice, - maxGasPrice: opts.MaxGasPrice, - replace: opts.Replace, - } -} - -// Modify implements the rpc.TXModifier interface. -func (e *LegacyGasFeeEstimator) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { - if !e.replace && tx.GasPrice != nil { - return nil - } - gasPrice, err := client.GasPrice(ctx) - if err != nil { - return fmt.Errorf("legacy gas fee estimator: failed to get gas price: %w", err) - } - gasPrice, _ = new(big.Float).Mul(new(big.Float).SetInt(gasPrice), big.NewFloat(e.multiplier)).Int(nil) - if e.minGasPrice != nil && gasPrice.Cmp(e.minGasPrice) < 0 { - gasPrice = e.minGasPrice - } - if e.maxGasPrice != nil && gasPrice.Cmp(e.maxGasPrice) > 0 { - gasPrice = e.maxGasPrice - } - tx.GasPrice = gasPrice - tx.MaxFeePerGas = nil - tx.MaxPriorityFeePerGas = nil - switch { - case tx.AccessList != nil: - tx.Type = types.AccessListTxType - default: - tx.Type = types.LegacyTxType - } - return nil -} - -// EIP1559GasFeeEstimator is a transaction modifier that estimates gas fee -// using the rpc.GasPrice and rpc.MaxPriorityFeePerGas methods. -// -// It sets transaction type to types.DynamicFeeTxType. -type EIP1559GasFeeEstimator struct { - gasPriceMultiplier float64 - priorityFeePerGasMultiplier float64 - minGasPrice *big.Int - maxGasPrice *big.Int - minPriorityFeePerGas *big.Int - maxPriorityFeePerGas *big.Int - replace bool -} - -// EIP1559GasFeeEstimatorOptions is the options for NewEIP1559GasFeeEstimator. -type EIP1559GasFeeEstimatorOptions struct { - GasPriceMultiplier float64 // GasPriceMultiplier is applied to the gas price. - PriorityFeePerGasMultiplier float64 // PriorityFeePerGasMultiplier is applied to the priority fee per gas. - MinGasPrice *big.Int // MinGasPrice is the minimum gas price, or nil if there is no lower bound. - MaxGasPrice *big.Int // MaxGasPrice is the maximum gas price, or nil if there is no upper bound. - MinPriorityFeePerGas *big.Int // MinPriorityFeePerGas is the minimum priority fee per gas, or nil if there is no lower bound. - MaxPriorityFeePerGas *big.Int // MaxPriorityFeePerGas is the maximum priority fee per gas, or nil if there is no upper bound. - Replace bool // Replace is true if the gas price should be replaced even if it is already set. -} - -// NewEIP1559GasFeeEstimator returns a new EIP1559GasFeeEstimator. -// -// To use this modifier, add it using the WithTXModifiers option when creating -// a new rpc.Client. -func NewEIP1559GasFeeEstimator(opts EIP1559GasFeeEstimatorOptions) *EIP1559GasFeeEstimator { - return &EIP1559GasFeeEstimator{ - gasPriceMultiplier: opts.GasPriceMultiplier, - priorityFeePerGasMultiplier: opts.PriorityFeePerGasMultiplier, - minGasPrice: opts.MinGasPrice, - maxGasPrice: opts.MaxGasPrice, - minPriorityFeePerGas: opts.MinPriorityFeePerGas, - maxPriorityFeePerGas: opts.MaxPriorityFeePerGas, - replace: opts.Replace, - } -} - -// Modify implements the rpc.TXModifier interface. -func (e *EIP1559GasFeeEstimator) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { - if !e.replace && tx.MaxFeePerGas != nil && tx.MaxPriorityFeePerGas != nil { - return nil - } - maxFeePerGas, err := client.GasPrice(ctx) - if err != nil { - return fmt.Errorf("EIP-1559 gas fee estimator: failed to get gas price: %w", err) - } - priorityFeePerGas, err := client.MaxPriorityFeePerGas(ctx) - if err != nil { - return fmt.Errorf("EIP-1559 gas fee estimator: failed to get max priority fee per gas: %w", err) - } - maxFeePerGas, _ = new(big.Float).Mul(new(big.Float).SetInt(maxFeePerGas), big.NewFloat(e.gasPriceMultiplier)).Int(nil) - priorityFeePerGas, _ = new(big.Float).Mul(new(big.Float).SetInt(priorityFeePerGas), big.NewFloat(e.priorityFeePerGasMultiplier)).Int(nil) - if e.minGasPrice != nil && maxFeePerGas.Cmp(e.minGasPrice) < 0 { - maxFeePerGas = e.minGasPrice - } - if e.maxGasPrice != nil && maxFeePerGas.Cmp(e.maxGasPrice) > 0 { - maxFeePerGas = e.maxGasPrice - } - if e.minPriorityFeePerGas != nil && priorityFeePerGas.Cmp(e.minPriorityFeePerGas) < 0 { - priorityFeePerGas = e.minPriorityFeePerGas - } - if e.maxPriorityFeePerGas != nil && priorityFeePerGas.Cmp(e.maxPriorityFeePerGas) > 0 { - priorityFeePerGas = e.maxPriorityFeePerGas - } - if maxFeePerGas.Cmp(priorityFeePerGas) < 0 { - priorityFeePerGas = maxFeePerGas - } - tx.GasPrice = nil - tx.MaxFeePerGas = maxFeePerGas - tx.MaxPriorityFeePerGas = priorityFeePerGas - tx.Type = types.DynamicFeeTxType - return nil -} diff --git a/txmodifier/gasfee_test.go b/txmodifier/gasfee_test.go deleted file mode 100644 index aa27699..0000000 --- a/txmodifier/gasfee_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package txmodifier - -import ( - "context" - "errors" - "math/big" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/defiweb/go-eth/types" -) - -func TestLegacyGasFeeEstimator_Modify(t *testing.T) { - ctx := context.Background() - - t.Run("successful gas fee estimation", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(1000), nil) - estimator := NewLegacyGasFeeEstimator(LegacyGasFeeEstimatorOptions{ - Multiplier: 1.5, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(1500), tx.GasPrice) - assert.Equal(t, types.LegacyTxType, tx.Type) - }) - - t.Run("gas fee estimation error", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return((*big.Int)(nil), errors.New("rpc error")) - - estimator := NewLegacyGasFeeEstimator(LegacyGasFeeEstimatorOptions{ - Multiplier: 1.5, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get gas price") - }) - - t.Run("gas fee below min bound", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(300), nil) - - estimator := NewLegacyGasFeeEstimator(LegacyGasFeeEstimatorOptions{ - Multiplier: 1.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(500), tx.GasPrice) // should be clamped to minGasPrice - }) - - t.Run("gas fee above max bound", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(2500), nil) - - estimator := NewLegacyGasFeeEstimator(LegacyGasFeeEstimatorOptions{ - Multiplier: 1.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(2000), tx.GasPrice) // should be clamped to maxGasPrice - }) -} - -func TestEIP1559GasFeeEstimator_Modify(t *testing.T) { - ctx := context.Background() - - t.Run("successful gas fee estimation", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(1000), nil) - rpcMock.On("MaxPriorityFeePerGas", ctx).Return(big.NewInt(5), nil) - - estimator := NewEIP1559GasFeeEstimator(EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.5, - PriorityFeePerGasMultiplier: 2.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - MinPriorityFeePerGas: big.NewInt(2), - MaxPriorityFeePerGas: big.NewInt(10), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(1500), tx.MaxFeePerGas) - assert.Equal(t, big.NewInt(10), tx.MaxPriorityFeePerGas) - assert.Equal(t, types.DynamicFeeTxType, tx.Type) - }) - - t.Run("gas fee estimation error (GasPrice call error)", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return((*big.Int)(nil), errors.New("rpc error")) - - estimator := NewEIP1559GasFeeEstimator(EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.5, - PriorityFeePerGasMultiplier: 2.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - MinPriorityFeePerGas: big.NewInt(2), - MaxPriorityFeePerGas: big.NewInt(10), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get gas price") - }) - - t.Run("gas fee estimation error (MaxPriorityFeePerGas call error)", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(1000), nil) - rpcMock.On("MaxPriorityFeePerGas", ctx).Return((*big.Int)(nil), errors.New("rpc error")) - - estimator := NewEIP1559GasFeeEstimator(EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.5, - PriorityFeePerGasMultiplier: 2.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - MinPriorityFeePerGas: big.NewInt(2), - MaxPriorityFeePerGas: big.NewInt(10), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get max priority fee per gas") - }) - - t.Run("gas fee below min bound", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(300), nil) - rpcMock.On("MaxPriorityFeePerGas", ctx).Return(big.NewInt(1), nil) - - estimator := NewEIP1559GasFeeEstimator(EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.0, - PriorityFeePerGasMultiplier: 1.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - MinPriorityFeePerGas: big.NewInt(2), - MaxPriorityFeePerGas: big.NewInt(10), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(500), tx.MaxFeePerGas) // should be clamped to minGasPrice - assert.Equal(t, big.NewInt(2), tx.MaxPriorityFeePerGas) // should be clamped to minPriorityFeePerGas - }) - - t.Run("gas fee above max bound", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(2500), nil) - rpcMock.On("MaxPriorityFeePerGas", ctx).Return(big.NewInt(12), nil) - - estimator := NewEIP1559GasFeeEstimator(EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.0, - PriorityFeePerGasMultiplier: 1.0, - MinGasPrice: big.NewInt(500), - MaxGasPrice: big.NewInt(2000), - MinPriorityFeePerGas: big.NewInt(2), - MaxPriorityFeePerGas: big.NewInt(10), - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(2000), tx.MaxFeePerGas) // should be clamped to maxGasPrice - assert.Equal(t, big.NewInt(10), tx.MaxPriorityFeePerGas) // should be clamped to maxPriorityFeePerGas - }) - - t.Run("gas tip fee higher than gas fee", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("GasPrice", ctx).Return(big.NewInt(500), nil) - rpcMock.On("MaxPriorityFeePerGas", ctx).Return(big.NewInt(2500), nil) - - estimator := NewEIP1559GasFeeEstimator(EIP1559GasFeeEstimatorOptions{ - GasPriceMultiplier: 1.0, - PriorityFeePerGasMultiplier: 1.0, - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, big.NewInt(500), tx.MaxFeePerGas) - assert.Equal(t, big.NewInt(500), tx.MaxPriorityFeePerGas) // should not be higher than tx.MaxFeePerGas - }) -} diff --git a/txmodifier/gaslimit.go b/txmodifier/gaslimit.go deleted file mode 100644 index f9c4891..0000000 --- a/txmodifier/gaslimit.go +++ /dev/null @@ -1,57 +0,0 @@ -package txmodifier - -import ( - "context" - "fmt" - "math/big" - - "github.com/defiweb/go-eth/rpc" - "github.com/defiweb/go-eth/types" -) - -// GasLimitEstimator is a transaction modifier that estimates gas limit -// using the rpc.EstimateGas method. -// -// To use this modifier, add it using the WithTXModifiers option when creating -// a new rpc.Client. -type GasLimitEstimator struct { - multiplier float64 - minGas uint64 - maxGas uint64 - replace bool -} - -// GasLimitEstimatorOptions is the options for NewGasLimitEstimator. -type GasLimitEstimatorOptions struct { - Multiplier float64 // Multiplier is applied to the gas limit. - MinGas uint64 // MinGas is the minimum gas limit, or 0 if there is no lower bound. - MaxGas uint64 // MaxGas is the maximum gas limit, or 0 if there is no upper bound. - Replace bool // Replace is true if the gas limit should be replaced even if it is already set. -} - -// NewGasLimitEstimator returns a new GasLimitEstimator. -func NewGasLimitEstimator(opts GasLimitEstimatorOptions) *GasLimitEstimator { - return &GasLimitEstimator{ - multiplier: opts.Multiplier, - minGas: opts.MinGas, - maxGas: opts.MaxGas, - replace: opts.Replace, - } -} - -// Modify implements the rpc.TXModifier interface. -func (e *GasLimitEstimator) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { - if !e.replace && tx.GasLimit != nil { - return nil - } - gasLimit, _, err := client.EstimateGas(ctx, &tx.Call, types.LatestBlockNumber) - if err != nil { - return fmt.Errorf("gas limit estimator: failed to estimate gas limit: %w", err) - } - gasLimit, _ = new(big.Float).Mul(new(big.Float).SetUint64(gasLimit), big.NewFloat(e.multiplier)).Uint64() - if gasLimit < e.minGas || (e.maxGas > 0 && gasLimit > e.maxGas) { - return fmt.Errorf("gas limit estimator: estimated gas limit %d is out of range [%d, %d]", gasLimit, e.minGas, e.maxGas) - } - tx.GasLimit = &gasLimit - return nil -} diff --git a/txmodifier/gaslimit_test.go b/txmodifier/gaslimit_test.go deleted file mode 100644 index abc0f02..0000000 --- a/txmodifier/gaslimit_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package txmodifier - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/defiweb/go-eth/types" -) - -func TestGasLimitEstimator_Modify(t *testing.T) { - ctx := context.Background() - - t.Run("successful gas estimation", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("EstimateGas", ctx, &tx.Call, types.LatestBlockNumber).Return(uint64(1000), &tx.Call, nil) - - estimator := NewGasLimitEstimator(GasLimitEstimatorOptions{ - Multiplier: 1.5, - MinGas: 500, - MaxGas: 2000, - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, uint64(1500), *tx.GasLimit) - }) - - t.Run("gas estimation error", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("EstimateGas", ctx, &tx.Call, types.LatestBlockNumber).Return(uint64(0), &tx.Call, errors.New("rpc error")) - - estimator := NewGasLimitEstimator(GasLimitEstimatorOptions{ - Multiplier: 1.5, - MinGas: 500, - MaxGas: 2000, - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to estimate gas") - }) - - t.Run("gas out of range", func(t *testing.T) { - tx := &types.Transaction{} - rpcMock := new(mockRPC) - rpcMock.On("EstimateGas", ctx, &tx.Call, types.LatestBlockNumber).Return(uint64(3000), &tx.Call, nil) - - estimator := NewGasLimitEstimator(GasLimitEstimatorOptions{ - Multiplier: 1.5, - MinGas: 500, - MaxGas: 2000, - }) - err := estimator.Modify(ctx, rpcMock, tx) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "estimated gas") - }) -} diff --git a/txmodifier/nonce.go b/txmodifier/nonce.go deleted file mode 100644 index 1856c22..0000000 --- a/txmodifier/nonce.go +++ /dev/null @@ -1,60 +0,0 @@ -package txmodifier - -import ( - "context" - "errors" - "fmt" - - "github.com/defiweb/go-eth/rpc" - "github.com/defiweb/go-eth/types" -) - -// NonceProvider is a transaction modifier that sets the nonce for the -// transaction. -// -// To use this modifier, add it using the WithTXModifiers option when creating -// a new rpc.Client. -type NonceProvider struct { - usePendingBlock bool - replace bool -} - -// NonceProviderOptions is the options for NewNonceProvider. -// -// If UsePendingBlock is true, then the next transaction nonce is fetched from -// the pending block. Otherwise, the next transaction nonce is fetched from the -// latest block. Using the pending block is not recommended as the behavior -// of the GetTransactionCount method on the pending block may be different -// between different Ethereum clients. -type NonceProviderOptions struct { - UsePendingBlock bool // UsePendingBlock indicates whether to use the pending block. - Replace bool // Replace is true if the nonce should be replaced even if it is already set. -} - -// NewNonceProvider returns a new NonceProvider. -func NewNonceProvider(opts NonceProviderOptions) *NonceProvider { - return &NonceProvider{ - usePendingBlock: opts.UsePendingBlock, - replace: opts.Replace, - } -} - -// Modify implements the rpc.TXModifier interface. -func (p *NonceProvider) Modify(ctx context.Context, client rpc.RPC, tx *types.Transaction) error { - if !p.replace && tx.Nonce != nil { - return nil - } - if tx.From == nil { - return errors.New("nonce provider: missing from address") - } - block := types.LatestBlockNumber - if p.usePendingBlock { - block = types.PendingBlockNumber - } - pendingNonce, err := client.GetTransactionCount(ctx, *tx.From, block) - if err != nil { - return fmt.Errorf("nonce provider: %w", err) - } - tx.Nonce = &pendingNonce - return nil -} diff --git a/txmodifier/nonce_test.go b/txmodifier/nonce_test.go deleted file mode 100644 index b526068..0000000 --- a/txmodifier/nonce_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package txmodifier - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/defiweb/go-eth/types" -) - -func TestNonceProvider_Modify(t *testing.T) { - ctx := context.Background() - fromAddress := types.MustAddressFromHex("0x1234567890abcdef1234567890abcdef12345678") - - t.Run("nonce fetch from latest block", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}} - rpcMock := new(mockRPC) - rpcMock.On("GetTransactionCount", ctx, fromAddress, types.LatestBlockNumber).Return(uint64(10), nil) - - provider := NewNonceProvider(NonceProviderOptions{ - UsePendingBlock: false, - }) - err := provider.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, uint64(10), *tx.Nonce) - }) - - t.Run("nonce fetch from pending block", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}} - rpcMock := new(mockRPC) - rpcMock.On("GetTransactionCount", ctx, fromAddress, types.PendingBlockNumber).Return(uint64(11), nil) - - provider := NewNonceProvider(NonceProviderOptions{ - UsePendingBlock: true, - }) - err := provider.Modify(ctx, rpcMock, tx) - - assert.NoError(t, err) - assert.Equal(t, uint64(11), *tx.Nonce) - }) - - t.Run("missing from address", func(t *testing.T) { - txWithoutFrom := &types.Transaction{} - provider := NewNonceProvider(NonceProviderOptions{ - UsePendingBlock: true, - }) - err := provider.Modify(ctx, nil, txWithoutFrom) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "nonce provider: missing from address") - }) - - t.Run("nonce fetch error", func(t *testing.T) { - tx := &types.Transaction{Call: types.Call{From: &fromAddress}} - rpcMock := new(mockRPC) - rpcMock.On("GetTransactionCount", ctx, fromAddress, types.LatestBlockNumber).Return(uint64(0), errors.New("rpc error")) - - provider := NewNonceProvider(NonceProviderOptions{ - UsePendingBlock: false, - }) - err := provider.Modify(ctx, rpcMock, tx) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "nonce provider") - }) -} diff --git a/txmodifier/txmodifier_test.go b/txmodifier/txmodifier_test.go deleted file mode 100644 index 71f2d71..0000000 --- a/txmodifier/txmodifier_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package txmodifier - -import ( - "context" - "math/big" - - "github.com/stretchr/testify/mock" - - "github.com/defiweb/go-eth/rpc" - "github.com/defiweb/go-eth/types" -) - -type mockRPC struct { - rpc.Client - mock.Mock -} - -func (m *mockRPC) ChainID(ctx context.Context) (uint64, error) { - args := m.Called(ctx) - return args.Get(0).(uint64), args.Error(1) -} - -func (m *mockRPC) EstimateGas(ctx context.Context, call *types.Call, block types.BlockNumber) (uint64, *types.Call, error) { - args := m.Called(ctx, call, block) - return args.Get(0).(uint64), call, args.Error(2) -} - -func (m *mockRPC) GasPrice(ctx context.Context) (*big.Int, error) { - args := m.Called(ctx) - return args.Get(0).(*big.Int), args.Error(1) -} - -func (m *mockRPC) MaxPriorityFeePerGas(ctx context.Context) (*big.Int, error) { - args := m.Called(ctx) - return args.Get(0).(*big.Int), args.Error(1) -} - -func (m *mockRPC) GetTransactionCount(ctx context.Context, address types.Address, block types.BlockNumber) (uint64, error) { - args := m.Called(ctx, address, block) - return args.Get(0).(uint64), args.Error(1) -} diff --git a/types/call.go b/types/call.go new file mode 100644 index 0000000..f3642bb --- /dev/null +++ b/types/call.go @@ -0,0 +1,30 @@ +package types + +import "encoding/json" + +// Call is an interface that represents a generic Ethereum call. +type Call interface { + json.Marshaler + json.Unmarshaler + + HasCallData +} + +type jsonCall struct { + From *Address `json:"from,omitempty"` + To *Address `json:"to,omitempty"` + GasLimit *Number `json:"gas,omitempty"` + GasPrice *Number `json:"gasPrice,omitempty"` + MaxFeePerGas *Number `json:"maxFeePerGas,omitempty"` + MaxFeePerBlobGas *Number `json:"maxFeePerBlobGas,omitempty"` + MaxPriorityFeePerGas *Number `json:"maxPriorityFeePerGas,omitempty"` + Input Bytes `json:"input,omitempty"` + Value *Number `json:"value,omitempty"` + AccessList AccessList `json:"accessList,omitempty"` + BlobHashes []Hash `json:"blobVersionedHashes,omitempty"` + Blobs []kzgBlob `json:"blobs,omitempty"` + Commitments []kzgCommitment `json:"commitments,omitempty"` + Proofs []kzgProof `json:"proofs,omitempty"` +} + +var _ Call = (*CallBasic)(nil) diff --git a/types/call_accesslist.go b/types/call_accesslist.go new file mode 100644 index 0000000..0808680 --- /dev/null +++ b/types/call_accesslist.go @@ -0,0 +1,55 @@ +package types + +import "encoding/json" + +// CallAccessList represents a call corresponding to the access list +// transaction type. +// +// Introduced by EIP-2930, this transaction type includes an optional access +// list that specifies a list of addresses and storage keys the transaction +// plans to access. +type CallAccessList struct { + CallData + LegacyPriceData + AccessListData +} + +// NewCallAccessList creates a new CallAccessList. +func NewCallAccessList() *CallAccessList { + return &CallAccessList{} +} + +// Copy creates a deep copy of the CallAccessList. +func (c *CallAccessList) Copy() *CallAccessList { + if c == nil { + return nil + } + return &CallAccessList{ + CallData: *c.CallData.Copy(), + LegacyPriceData: *c.LegacyPriceData.Copy(), + AccessListData: *c.AccessListData.Copy(), + } +} + +// MarshalJSON implements the json.Marshaler interface. +func (c *CallAccessList) MarshalJSON() ([]byte, error) { + j := &jsonCall{} + c.CallData.toJSON(j) + c.LegacyPriceData.toJSON(j) + c.AccessListData.toJSON(j) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *CallAccessList) UnmarshalJSON(data []byte) error { + j := &jsonCall{} + if err := json.Unmarshal(data, j); err != nil { + return err + } + c.CallData.fromJSON(j) + c.LegacyPriceData.fromJSON(j) + c.AccessListData.fromJSON(j) + return nil +} + +var _ Call = (*CallAccessList)(nil) diff --git a/types/call_accesslist_test.go b/types/call_accesslist_test.go new file mode 100644 index 0000000..850c23f --- /dev/null +++ b/types/call_accesslist_test.go @@ -0,0 +1,81 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCallAccessList_JSON(t *testing.T) { + tests := []struct { + name string + call *CallAccessList + wantJSON string + }{ + { + name: "all fields nil", + call: &CallAccessList{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + call: &CallAccessList{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + wantJSON: `{ + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "value": "0xde0b6b3a7640000", + "gas": "0x186a0", + "input": "0x01020304", + "gasPrice": "0x3b9aca00", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.call.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Decode from JSON + call := NewCallAccessList() + err = call.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded call + call.From = tt.call.From + assertEqualCall(t, call, tt.call) + }) + } +} diff --git a/types/call_basic.go b/types/call_basic.go new file mode 100644 index 0000000..24258db --- /dev/null +++ b/types/call_basic.go @@ -0,0 +1,40 @@ +package types + +import "encoding/json" + +// CallBasic represents a simplest Ethereum call. +type CallBasic struct { + CallData +} + +// NewCall creates a new [CallBasic] instance. +func NewCall() *CallBasic { + // It is called NewCall instead of NewCallBasic because, most of the time, + // people do not care about the call type; they just want to execute a + // simple call without any special features. + return &CallBasic{} +} + +// Copy creates a deep copy of the CallBasic. +func (c *CallBasic) Copy() *CallBasic { + return &CallBasic{ + CallData: *c.CallData.Copy(), + } +} + +// MarshalJSON implements the json.Marshaler interface. +func (c CallBasic) MarshalJSON() ([]byte, error) { + j := &jsonCall{} + c.CallData.toJSON(j) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c CallBasic) UnmarshalJSON(bytes []byte) error { + j := &jsonCall{} + if err := json.Unmarshal(bytes, &j); err != nil { + return err + } + c.CallData.fromJSON(j) + return nil +} diff --git a/types/call_blob.go b/types/call_blob.go new file mode 100644 index 0000000..ebc01e8 --- /dev/null +++ b/types/call_blob.go @@ -0,0 +1,59 @@ +package types + +import ( + "encoding/json" +) + +// CallBlob represents a call corresponding to the blob transaction type. +// +// Introduced by EIP-4844, this transaction type adds support for blob-carrying +// transactions. +type CallBlob struct { + CallData + AccessListData + DynamicFeeData + BlobData +} + +// NewCallBlob creates a new CallBlob. +func NewCallBlob() *CallBlob { + return &CallBlob{} +} + +// Copy creates a deep copy of the CallBlob. +func (c *CallBlob) Copy() *CallBlob { + if c == nil { + return nil + } + return &CallBlob{ + CallData: *c.CallData.Copy(), + AccessListData: *c.AccessListData.Copy(), + DynamicFeeData: *c.DynamicFeeData.Copy(), + BlobData: *c.BlobData.Copy(), + } +} + +// MarshalJSON implements the json.Marshaler interface. +func (c *CallBlob) MarshalJSON() ([]byte, error) { + j := &jsonCall{} + c.CallData.toJSON(j) + c.AccessListData.toJSON(j) + c.DynamicFeeData.toJSON(j) + c.BlobData.toJSON(j) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *CallBlob) UnmarshalJSON(data []byte) error { + j := &jsonCall{} + if err := json.Unmarshal(data, &j); err != nil { + return err + } + c.CallData.fromJSON(j) + c.AccessListData.fromJSON(j) + c.DynamicFeeData.fromJSON(j) + c.BlobData.fromJSON(j) + return nil +} + +var _ Call = (*CallBlob)(nil) diff --git a/types/call_blob_test.go b/types/call_blob_test.go new file mode 100644 index 0000000..923f8c2 --- /dev/null +++ b/types/call_blob_test.go @@ -0,0 +1,164 @@ +package types + +import ( + "math/big" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCallBlob_JSON(t *testing.T) { + remZerosRx := regexp.MustCompile(`0{128,}`) + tests := []struct { + name string + call *CallBlob + wantJSON string + }{ + { + name: "empty call", + call: &CallBlob{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + call: &CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + {Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone)}, + {Hash: MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", PadNone)}, + }, + }, + }, + wantJSON: `{ + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxFeePerBlobGas": "0xb2d05e00", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "blobVersionedHashes": [ + "0x6666666666666666666666666666666666666666666666666666666666666666", + "0x7777777777777777777777777777777777777777777777777777777777777777" + ] + }`, + }, + { + name: "blobs with shortened zero fields", + call: &CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + newBlob("blob1"), + newBlob("blob2"), + }, + }, + }, + wantJSON: `{ + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxFeePerBlobGas": "0xb2d05e00", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "blobVersionedHashes": [ + "0x01e951827dab35ecb4ce6e29ca6779ad0ac958f06ac1d54eb6e7523f3e3febeb", + "0x01c6f423eec4f5e9ebd79e6fb5eb9bce57dd102f2f0a6fdee0bf58c4e109e27a" + ], + "blobs": [ + "0x626c6f6231", + "0x626c6f6232" + ], + "commitments": [ + "0x832726ece34fb93100194291b75b7f5fa920d5b896e5edafeed46f3636fadc485f493490bd596c76171516024bcf7a00", + "0x896a515deb6c1ac23436f5de75186316b53851be3cb4225437e58c34e376bbd6dea13d7a39b8d5dd9f6cba1c052ab0cb" + ], + "proofs": [ + "0xacbe7bde870d1e7c239063368dbf62ce3bfaef1c08006e8e724da2837294e19d991ba5677a628e64b6c6906ff1786b3c", + "0x8fd7fb4172048b9d8deccb939eeb787c45d0bb00d5f238e13084bd3fbe8050215ce488999f86fccddc36d1a257a19513" + ] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.call.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(remZerosRx.ReplaceAll(jsonBytes, []byte("")))) + + // Decode from JSON + tx := NewCallBlob() + err = tx.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded calls + tx.From = tt.call.From + assertEqualCall(t, tx, tt.call) + }) + } +} diff --git a/types/call_dynamicfee.go b/types/call_dynamicfee.go new file mode 100644 index 0000000..f2620f7 --- /dev/null +++ b/types/call_dynamicfee.go @@ -0,0 +1,54 @@ +package types + +import "encoding/json" + +// CallDynamicFee represents a call corresponding to the dynamic fee +// transaction type. +// +// Introduced by EIP-1559, this transaction type supports a new fee market +// mechanism with a base fee and a priority fee (tip). +type CallDynamicFee struct { + CallData + DynamicFeeData + AccessListData +} + +// NewCallDynamicFee creates a new CallDynamicFee. +func NewCallDynamicFee() *CallDynamicFee { + return &CallDynamicFee{} +} + +// Copy creates a deep copy of the CallDynamicFee. +func (c *CallDynamicFee) Copy() *CallDynamicFee { + if c == nil { + return nil + } + return &CallDynamicFee{ + CallData: *c.CallData.Copy(), + DynamicFeeData: *c.DynamicFeeData.Copy(), + AccessListData: *c.AccessListData.Copy(), + } +} + +// MarshalJSON implements the json.Marshaler interface. +func (c *CallDynamicFee) MarshalJSON() ([]byte, error) { + j := &jsonCall{} + c.CallData.toJSON(j) + c.DynamicFeeData.toJSON(j) + c.AccessListData.toJSON(j) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *CallDynamicFee) UnmarshalJSON(data []byte) error { + j := &jsonCall{} + if err := json.Unmarshal(data, &j); err != nil { + return err + } + c.CallData.fromJSON(j) + c.DynamicFeeData.fromJSON(j) + c.AccessListData.fromJSON(j) + return nil +} + +var _ Call = (*CallDynamicFee)(nil) diff --git a/types/call_dynamicfee_test.go b/types/call_dynamicfee_test.go new file mode 100644 index 0000000..3e31b4d --- /dev/null +++ b/types/call_dynamicfee_test.go @@ -0,0 +1,83 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCallDynamicFee_JSON(t *testing.T) { + tests := []struct { + name string + call *CallDynamicFee + wantJSON string + }{ + { + name: "empty call", + call: &CallDynamicFee{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + call: &CallDynamicFee{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + wantJSON: `{ + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.call.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Decode from JSON + tx := NewCallDynamicFee() + err = tx.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded calls + tx.From = tt.call.From + assertEqualCall(t, tx, tt.call) + }) + } +} diff --git a/types/call_legacy.go b/types/call_legacy.go new file mode 100644 index 0000000..ee3a2df --- /dev/null +++ b/types/call_legacy.go @@ -0,0 +1,46 @@ +package types + +import "encoding/json" + +// CallLegacy represents a call corresponding to the legacy transaction type. +type CallLegacy struct { + CallData + LegacyPriceData +} + +// NewCallLegacy creates a new CallLegacy. +func NewCallLegacy() *CallLegacy { + return &CallLegacy{} +} + +// Copy creates a deep copy of the CallLegacy. +func (c *CallLegacy) Copy() *CallLegacy { + if c == nil { + return nil + } + return &CallLegacy{ + CallData: *c.CallData.Copy(), + LegacyPriceData: *c.LegacyPriceData.Copy(), + } +} + +// MarshalJSON implements the json.Marshaler interface. +func (c CallLegacy) MarshalJSON() ([]byte, error) { + j := &jsonCall{} + c.CallData.toJSON(j) + c.LegacyPriceData.toJSON(j) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *CallLegacy) UnmarshalJSON(data []byte) error { + j := &jsonCall{} + if err := json.Unmarshal(data, &j); err != nil { + return err + } + c.CallData.fromJSON(j) + c.LegacyPriceData.fromJSON(j) + return nil +} + +var _ Call = (*CallLegacy)(nil) diff --git a/types/call_legacy_test.go b/types/call_legacy_test.go new file mode 100644 index 0000000..0f6f09f --- /dev/null +++ b/types/call_legacy_test.go @@ -0,0 +1,63 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCallLegacy_JSON(t *testing.T) { + tests := []struct { + name string + call *CallLegacy + wantJSON string + }{ + { + name: "all fields nil", + call: &CallLegacy{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + call: &CallLegacy{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + wantJSON: `{ + "gasPrice": "0x3b9aca00", + "gas": "0x186a0", + "to": "0x2222222222222222222222222222222222222222", + "value": "0xde0b6b3a7640000", + "input": "0x01020304", + "from": "0x1111111111111111111111111111111111111111" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.call.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Decode from JSON + call := NewCallLegacy() + err = call.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded call + call.From = tt.call.From + assertEqualCall(t, call, tt.call) + }) + } +} diff --git a/types/embed.go b/types/embed.go new file mode 100644 index 0000000..b4a09fd --- /dev/null +++ b/types/embed.go @@ -0,0 +1,448 @@ +package types + +import ( + "math/big" + + "github.com/defiweb/go-eth/crypto/kzg4844" +) + +// The following interfaces are used to determine if a given call or +// transaction has specific capabilities. + +// HasTransactionData specifes that the type has basic transaction +// data fields like chain ID, nonce, and signature. +type HasTransactionData interface { + GetTransactionData() *TransactionData + SetTransactionData(data TransactionData) +} + +// HasCallData specifies that the type has basic call data fields +// like from, to, gas limit, value, and input. +type HasCallData interface { + GetCallData() *CallData + SetCallData(data CallData) +} + +// HasLegacyPriceData specifies that the type uses legacy price data. +type HasLegacyPriceData interface { + GetLegacyPriceData() *LegacyPriceData + SetLegacyPriceData(data LegacyPriceData) +} + +// HasAccessListData specifies that the type uses access list for EIP-2930 +// transactions. +type HasAccessListData interface { + GetAccessListData() *AccessListData + SetAccessListData(data AccessListData) +} + +// HasDynamicFeeData specifies that the type uses dynamic fee data for EIP-1559 +// transactions. +type HasDynamicFeeData interface { + GetDynamicFeeData() *DynamicFeeData + SetDynamicFeeData(data DynamicFeeData) +} + +// HasBlobData specifies that the type uses blob data for EIP-4844 +// transactions. +type HasBlobData interface { + GetBlobData() *BlobData + SetBlobData(data BlobData) +} + +// The following types are used to embed common fields and methods into call +// and transaction types. You probably do not want to use them directly. + +// TransactionData contains common fields for transactions. +// +// This type is used to embed transaction data into other types. +type TransactionData struct { + ChainID *uint64 // ChainID is the chain ID. + Nonce *uint64 // Nonce is the transaction nonce. + Signature *Signature // Signature is the transaction signature. +} + +// GetTransactionData returns the embedded transaction data. +func (c *TransactionData) GetTransactionData() *TransactionData { + return c +} + +// SetTransactionData sets the embedded transaction data. +func (c *TransactionData) SetTransactionData(data TransactionData) { + *c = data +} + +// SetChainID sets the chain ID. +func (c *TransactionData) SetChainID(chainID uint64) { + c.ChainID = &chainID +} + +// SetNonce sets the transaction nonce. +func (c *TransactionData) SetNonce(nonce uint64) { + c.Nonce = &nonce +} + +// SetSignature sets the transaction signature. +func (c *TransactionData) SetSignature(signature Signature) { + c.Signature = &signature +} + +// Copy creates a deep copy of the TransactionFields. +func (c *TransactionData) Copy() *TransactionData { + return &TransactionData{ + ChainID: copyPtr(c.ChainID), + Nonce: copyPtr(c.Nonce), + Signature: c.Signature.Copy(), + } +} + +func (c *TransactionData) toJSON(j *jsonTransaction) { + if c.ChainID != nil { + j.ChainID = NumberFromUint64Ptr(*c.ChainID) + } + if c.Nonce != nil { + j.Nonce = NumberFromUint64Ptr(*c.Nonce) + } + if c.Signature != nil { + j.V = NumberFromBigIntPtr(c.Signature.V) + j.R = NumberFromBigIntPtr(c.Signature.R) + j.S = NumberFromBigIntPtr(c.Signature.S) + } +} + +func (c *TransactionData) fromJSON(j *jsonTransaction) { + if j.ChainID != nil { + chainID := j.ChainID.Big().Uint64() + c.ChainID = &chainID + } + if j.Nonce != nil { + nonce := j.Nonce.Big().Uint64() + c.Nonce = &nonce + } + if j.V != nil || j.R != nil || j.S != nil { + c.Signature = SignatureFromVRSPtr(j.V.Big(), j.R.Big(), j.S.Big()) + } +} + +// CallData contains the basic fields for a call. +// +// This type is used to embed call data into other types. +type CallData struct { + From *Address // From is the sender address. + To *Address // To is the recipient address. Nil means contract creation. + GasLimit *uint64 // GasLimit is the gas limit; if 0, there is no limit. + Value *big.Int // Value is the amount of wei to send. + Input []byte // Input is the input data. +} + +// GetCallData returns the embedded call data. +func (c *CallData) GetCallData() *CallData { + return c +} + +// SetCallData sets the embedded call data. +func (c *CallData) SetCallData(data CallData) { + *c = data +} + +// SetFrom sets the sender address. +func (c *CallData) SetFrom(from Address) { + c.From = &from +} + +// SetTo sets the recipient address. +func (c *CallData) SetTo(to Address) { + c.To = &to +} + +// SetGasLimit sets the gas limit. +func (c *CallData) SetGasLimit(gasLimit uint64) { + c.GasLimit = &gasLimit +} + +// SetValue sets the amount of wei to send. +func (c *CallData) SetValue(value *big.Int) { + c.Value = value +} + +// SetInput sets the input data. +func (c *CallData) SetInput(input []byte) { + c.Input = input +} + +// Copy creates a deep copy of the CallFields. +func (c *CallData) Copy() *CallData { + if c == nil { + return nil + } + return &CallData{ + From: copyPtr(c.From), + To: copyPtr(c.To), + GasLimit: copyPtr(c.GasLimit), + Value: copyBigInt(c.Value), + Input: copyBytes(c.Input), + } +} + +func (c *CallData) toJSON(j *jsonCall) { + j.From = c.From + j.To = c.To + if c.GasLimit != nil { + j.GasLimit = NumberFromUint64Ptr(*c.GasLimit) + } + if c.Value != nil { + j.Value = NumberFromBigIntPtr(c.Value) + } + j.Input = c.Input +} + +func (c *CallData) fromJSON(j *jsonCall) { + c.From = j.From + c.To = j.To + if j.GasLimit != nil { + gas := j.GasLimit.Big().Uint64() + c.GasLimit = &gas + } + if j.Value != nil { + c.Value = j.Value.Big() + } + c.Input = j.Input +} + +// LegacyPriceData contains the gas price for legacy transactions. +// +// This type is used to embed legacy price data into other types. +type LegacyPriceData struct { + GasPrice *big.Int // GasPrice is the gas price. +} + +// LegacyPriceData returns the embedded legacy price data. +func (c *LegacyPriceData) GetLegacyPriceData() *LegacyPriceData { + return c +} + +// SetLegacyPriceData sets the embedded legacy price data. +func (c *LegacyPriceData) SetLegacyPriceData(data LegacyPriceData) { + *c = data +} + +// SetGasPrice sets the gas price. +func (c *LegacyPriceData) SetGasPrice(gasPrice *big.Int) { + c.GasPrice = gasPrice +} + +// Copy creates a deep copy of the LegacyPriceField. +func (c *LegacyPriceData) Copy() *LegacyPriceData { + if c == nil { + return nil + } + return &LegacyPriceData{ + GasPrice: copyBigInt(c.GasPrice), + } +} + +func (c *LegacyPriceData) toJSON(j *jsonCall) { + if c.GasPrice != nil { + j.GasPrice = NumberFromBigIntPtr(c.GasPrice) + } +} + +func (c *LegacyPriceData) fromJSON(j *jsonCall) { + if j.GasPrice != nil { + c.GasPrice = j.GasPrice.Big() + } +} + +// AccessListData contains the access list for EIP-2930 transactions. +// +// This type is used to embed access list data into other types. +type AccessListData struct { + AccessList AccessList // AccessList is the EIP-2930 access list. +} + +// AccessListData returns the embedded access list data. +func (c *AccessListData) GetAccessListData() *AccessListData { + return c +} + +// SetAccessListData sets the embedded access list data. +func (c *AccessListData) SetAccessListData(data AccessListData) { + *c = data +} + +// SetAccessList sets the access list. +func (c *AccessListData) SetAccessList(accessList AccessList) { + c.AccessList = accessList +} + +// Copy creates a deep copy of the AccessListField. +func (c *AccessListData) Copy() *AccessListData { + if c == nil { + return nil + } + return &AccessListData{ + AccessList: c.AccessList.Copy(), + } +} + +func (c *AccessListData) toJSON(j *jsonCall) { + j.AccessList = c.AccessList +} + +func (c *AccessListData) fromJSON(j *jsonCall) { + c.AccessList = j.AccessList +} + +// DynamicFeeData contains fee data for EIP-1559 transactions. +// +// This type is used to embed dynamic fee data into other types. +type DynamicFeeData struct { + MaxFeePerGas *big.Int // MaxFeePerGas is the maximum total fee per gas. + MaxPriorityFeePerGas *big.Int // MaxPriorityFeePerGas is the maximum priority fee per gas. +} + +// DynamicFeeData returns the embedded dynamic fee data. +func (c *DynamicFeeData) GetDynamicFeeData() *DynamicFeeData { + return c +} + +// SetDynamicFeeData sets the embedded dynamic fee data. +func (c *DynamicFeeData) SetDynamicFeeData(data DynamicFeeData) { + *c = data +} + +// SetMaxFeePerGas sets the maximum total fee per gas. +func (c *DynamicFeeData) SetMaxFeePerGas(maxFeePerGas *big.Int) { + c.MaxFeePerGas = maxFeePerGas +} + +// SetMaxPriorityFeePerGas sets the maximum priority fee per gas. +func (c *DynamicFeeData) SetMaxPriorityFeePerGas(maxPriorityFeePerGas *big.Int) { + c.MaxPriorityFeePerGas = maxPriorityFeePerGas +} + +// Copy creates a deep copy of the DynamicFeeFields. +func (c *DynamicFeeData) Copy() *DynamicFeeData { + if c == nil { + return nil + } + return &DynamicFeeData{ + MaxFeePerGas: copyBigInt(c.MaxFeePerGas), + MaxPriorityFeePerGas: copyBigInt(c.MaxPriorityFeePerGas), + } +} + +func (c *DynamicFeeData) toJSON(j *jsonCall) { + if c.MaxFeePerGas != nil { + j.MaxFeePerGas = NumberFromBigIntPtr(c.MaxFeePerGas) + } + if c.MaxPriorityFeePerGas != nil { + j.MaxPriorityFeePerGas = NumberFromBigIntPtr(c.MaxPriorityFeePerGas) + } +} + +func (c *DynamicFeeData) fromJSON(j *jsonCall) { + if j.MaxFeePerGas != nil { + c.MaxFeePerGas = j.MaxFeePerGas.Big() + } + if j.MaxPriorityFeePerGas != nil { + c.MaxPriorityFeePerGas = j.MaxPriorityFeePerGas.Big() + } +} + +// BlobData contains data for EIP-4844 blob transactions. +// +// Use NewBlobInfo to create a BlobInfo. +// +// This type is used to embed blob data into other types. +type BlobData struct { + MaxFeePerBlobGas *big.Int // MaxFeePerBlobGas is the maximum fee per blob gas. + Blobs []BlobInfo // Blobs is the list of blobs. +} + +// BlobData returns the embedded blob data. +func (c *BlobData) GetBlobData() *BlobData { + return c +} + +// SetBlobData sets the embedded blob data. +func (c *BlobData) SetBlobData(data BlobData) { + *c = data +} + +// SetMaxFeePerBlobGas sets the maximum fee per blob gas. +func (c *BlobData) SetMaxFeePerBlobGas(maxFeePerBlobGas *big.Int) { + c.MaxFeePerBlobGas = maxFeePerBlobGas +} + +// SetBlobs sets the list of blobs. +// +// Use NewBlobInfo to create a BlobInfo. +func (c *BlobData) SetBlobs(blobs []BlobInfo) { + c.Blobs = blobs +} + +// AddBlob adds a blob to the list. +// +// Use NewBlobInfo to create a BlobInfo. +func (c *BlobData) AddBlob(blob BlobInfo) { + c.Blobs = append(c.Blobs, blob) +} + +// Copy creates a deep copy of the BlobFields. +func (c *BlobData) Copy() *BlobData { + if c == nil { + return nil + } + blobs := make([]BlobInfo, len(c.Blobs)) + for i, blob := range c.Blobs { + blobs[i].Hash = blob.Hash + blobs[i].Sidecar = copyPtr(blob.Sidecar) + } + return &BlobData{ + MaxFeePerBlobGas: copyBigInt(c.MaxFeePerBlobGas), + Blobs: blobs, + } +} + +func (c *BlobData) toJSON(j *jsonCall) { + if c.MaxFeePerBlobGas != nil { + j.MaxFeePerBlobGas = NumberFromBigIntPtr(c.MaxFeePerBlobGas) + } + if len(c.Blobs) > 0 && c.Blobs[0].Sidecar != nil { + // If the first blob has a sidecar, then all blobs should have + // sidecars, so we can allocate memory for them. + j.BlobHashes = make([]Hash, 0, len(c.Blobs)) + j.Blobs = make([]kzgBlob, 0, len(c.Blobs)) + j.Commitments = make([]kzgCommitment, 0, len(c.Blobs)) + j.Proofs = make([]kzgProof, 0, len(c.Blobs)) + } + for _, b := range c.Blobs { + j.BlobHashes = append(j.BlobHashes, b.Hash) + if b.Sidecar != nil { + j.Blobs = append(j.Blobs, kzgBlob(b.Sidecar.Blob)) + j.Commitments = append(j.Commitments, kzgCommitment(b.Sidecar.Commitment)) + j.Proofs = append(j.Proofs, kzgProof(b.Sidecar.Proof)) + } + } +} + +func (c *BlobData) fromJSON(j *jsonCall) { + if j.MaxFeePerBlobGas != nil { + c.MaxFeePerBlobGas = j.MaxFeePerBlobGas.Big() + } + if len(j.BlobHashes) > 0 { + c.Blobs = make([]BlobInfo, len(j.BlobHashes)) + for i, h := range j.BlobHashes { + b := BlobInfo{Hash: h} + if i < len(j.Blobs) && i < len(j.Commitments) && i < len(j.Proofs) { + b.Sidecar = &BlobSidecar{ + Blob: kzg4844.Blob(j.Blobs[i]), + Commitment: kzg4844.Commitment(j.Commitments[i]), + Proof: kzg4844.Proof(j.Proofs[i]), + } + } + c.Blobs[i] = b + } + } +} diff --git a/types/embed_test.go b/types/embed_test.go new file mode 100644 index 0000000..53339bc --- /dev/null +++ b/types/embed_test.go @@ -0,0 +1,592 @@ +package types + +import ( + "encoding/json" + "math/big" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/defiweb/go-eth/crypto/kzg4844" +) + +func TestTransactionData_JSON(t *testing.T) { + tests := []struct { + name string + data *TransactionData + want *jsonTransaction + wantJSON string + }{ + { + name: "all fields nil", + data: &TransactionData{}, + want: &jsonTransaction{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + data: &TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(2)), + Signature: &Signature{ + V: big.NewInt(27), + R: big.NewInt(123456), + S: big.NewInt(654321), + }, + }, + want: &jsonTransaction{ + ChainID: NumberFromUint64Ptr(1), + Nonce: NumberFromUint64Ptr(2), + V: NumberFromBigIntPtr(big.NewInt(27)), + R: NumberFromBigIntPtr(big.NewInt(123456)), + S: NumberFromBigIntPtr(big.NewInt(654321)), + }, + wantJSON: `{ + "chainId": "0x1", + "nonce": "0x2", + "v": "0x1b", + "r": "0x1e240", + "s": "0x9fbf1" + }`, + }, + { + name: "nil signature", + data: &TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(2)), + Signature: nil, + }, + want: &jsonTransaction{ + ChainID: NumberFromUint64Ptr(1), + Nonce: NumberFromUint64Ptr(2), + V: nil, + R: nil, + S: nil, + }, + wantJSON: `{ + "chainId": "0x1", + "nonce": "0x2" + }`, + }, + { + name: "max uint64 values", + data: &TransactionData{ + ChainID: ptr(^uint64(0)), + Nonce: ptr(^uint64(0)), + }, + want: &jsonTransaction{ + ChainID: NumberFromUint64Ptr(^uint64(0)), + Nonce: NumberFromUint64Ptr(^uint64(0)), + }, + wantJSON: `{ + "chainId": "0xffffffffffffffff", + "nonce": "0xffffffffffffffff" + }`, + }, + { + name: "zero values", + data: &TransactionData{ + ChainID: ptr(uint64(0)), + Nonce: ptr(uint64(0)), + Signature: &Signature{ + V: big.NewInt(0), + R: big.NewInt(0), + S: big.NewInt(0), + }, + }, + want: &jsonTransaction{ + ChainID: NumberFromUint64Ptr(0), + Nonce: NumberFromUint64Ptr(0), + V: NumberFromBigIntPtr(big.NewInt(0)), + R: NumberFromBigIntPtr(big.NewInt(0)), + S: NumberFromBigIntPtr(big.NewInt(0)), + }, + wantJSON: `{ + "chainId": "0x0", + "nonce": "0x0", + "v": "0x0", + "r": "0x0", + "s": "0x0" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test toJSON + var j jsonTransaction + tt.data.toJSON(&j) + assert.Equal(t, tt.want.ChainID, j.ChainID) + assert.Equal(t, tt.want.Nonce, j.Nonce) + assert.Equal(t, tt.want.V, j.V) + assert.Equal(t, tt.want.R, j.R) + assert.Equal(t, tt.want.S, j.S) + + // Verify generated JSON string + jsonBytes, err := json.Marshal(j) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Test fromJSON + var data TransactionData + err = json.Unmarshal(jsonBytes, &j) + assert.NoError(t, err) + data.fromJSON(&j) + assert.Equal(t, tt.data.ChainID, data.ChainID) + assert.Equal(t, tt.data.Nonce, data.Nonce) + if tt.data.Signature == nil { + assert.Nil(t, data.Signature) + } else { + assert.NotNil(t, data.Signature) + assert.Equal(t, tt.data.Signature.V, data.Signature.V) + assert.Equal(t, tt.data.Signature.R, data.Signature.R) + assert.Equal(t, tt.data.Signature.S, data.Signature.S) + } + }) + } +} + +func TestCallData_JSON(t *testing.T) { + tests := []struct { + name string + data *CallData + want *jsonCall + wantJSON string + }{ + { + name: "all fields nil", + data: &CallData{}, + want: &jsonCall{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + data: &CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + GasLimit: ptr(uint64(21000)), + Value: big.NewInt(1000000000000000000), + Input: []byte{0x01, 0x02, 0x03}, + }, + want: &jsonCall{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + GasLimit: NumberFromUint64Ptr(21000), + Value: NumberFromBigIntPtr(big.NewInt(1000000000000000000)), + Input: []byte{0x01, 0x02, 0x03}, + }, + wantJSON: `{ + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x5208", + "value": "0xde0b6b3a7640000", + "input": "0x010203" + }`, + }, + { + name: "max values", + data: &CallData{ + From: MustAddressFromHexPtr("0xffffffffffffffffffffffffffffffffffffffff"), + To: MustAddressFromHexPtr("0xffffffffffffffffffffffffffffffffffffffff"), + GasLimit: ptr(^uint64(0)), + Value: big.NewInt(0).Sub(big.NewInt(0).SetBit(big.NewInt(0), 256, 1), big.NewInt(1)), + Input: []byte{0x01, 0x02, 0x03}, + }, + want: &jsonCall{ + From: MustAddressFromHexPtr("0xffffffffffffffffffffffffffffffffffffffff"), + To: MustAddressFromHexPtr("0xffffffffffffffffffffffffffffffffffffffff"), + GasLimit: NumberFromUint64Ptr(^uint64(0)), + Value: NumberFromBigIntPtr(big.NewInt(0).Sub(big.NewInt(0).SetBit(big.NewInt(0), 256, 1), big.NewInt(1))), + Input: []byte{0x01, 0x02, 0x03}, + }, + wantJSON: `{ + "from": "0xffffffffffffffffffffffffffffffffffffffff", + "to": "0xffffffffffffffffffffffffffffffffffffffff", + "gas": "0xffffffffffffffff", + "value": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "input": "0x010203" + }`, + }, + { + name: "zero values", + data: &CallData{ + From: MustAddressFromHexPtr("0x0000000000000000000000000000000000000000"), + To: MustAddressFromHexPtr("0x0000000000000000000000000000000000000000"), + GasLimit: ptr(uint64(0)), + Value: big.NewInt(0), + Input: []byte{}, + }, + want: &jsonCall{ + From: MustAddressFromHexPtr("0x0000000000000000000000000000000000000000"), + To: MustAddressFromHexPtr("0x0000000000000000000000000000000000000000"), + GasLimit: NumberFromUint64Ptr(0), + Value: NumberFromBigIntPtr(big.NewInt(0)), + Input: []byte{}, + }, + wantJSON: `{ + "from": "0x0000000000000000000000000000000000000000", + "to": "0x0000000000000000000000000000000000000000", + "gas": "0x0", + "value": "0x0" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test toJSON + var j jsonCall + tt.data.toJSON(&j) + assert.Equal(t, tt.want.From, j.From) + assert.Equal(t, tt.want.To, j.To) + assert.Equal(t, tt.want.GasLimit, j.GasLimit) + assert.Equal(t, tt.want.Value, j.Value) + assert.Equal(t, tt.want.Input, j.Input) + + // Verify generated JSON string + jsonBytes, err := json.Marshal(j) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Test fromJSON + var data CallData + err = json.Unmarshal(jsonBytes, &j) + assert.NoError(t, err) + data.fromJSON(&j) + assert.Equal(t, tt.data.From, data.From) + assert.Equal(t, tt.data.To, data.To) + assert.Equal(t, tt.data.GasLimit, data.GasLimit) + assert.Equal(t, tt.data.Value, data.Value) + assert.Equal(t, tt.data.Input, data.Input) + }) + } +} + +func TestLegacyPriceData_JSON(t *testing.T) { + tests := []struct { + name string + data *LegacyPriceData + want *jsonCall + wantJSON string + }{ + { + name: "all fields nil", + data: &LegacyPriceData{}, + want: &jsonCall{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + data: &LegacyPriceData{ + GasPrice: big.NewInt(2000000000), + }, + want: &jsonCall{ + GasPrice: NumberFromBigIntPtr(big.NewInt(2000000000)), + }, + wantJSON: `{ + "gasPrice": "0x77359400" + }`, + }, + { + name: "zero value", + data: &LegacyPriceData{ + GasPrice: big.NewInt(0), + }, + want: &jsonCall{ + GasPrice: NumberFromBigIntPtr(big.NewInt(0)), + }, + wantJSON: `{ + "gasPrice": "0x0" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test toJSON + var j jsonCall + tt.data.toJSON(&j) + assert.Equal(t, tt.want.GasPrice, j.GasPrice) + + // Verify generated JSON string + jsonBytes, err := json.Marshal(j) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Test fromJSON + var data LegacyPriceData + err = json.Unmarshal(jsonBytes, &j) + assert.NoError(t, err) + data.fromJSON(&j) + assert.Equal(t, tt.data.GasPrice, data.GasPrice) + }) + } +} + +func TestAccessListData_JSON(t *testing.T) { + tests := []struct { + name string + data *AccessListData + want *jsonCall + wantJSON string + }{ + { + name: "all fields nil", + data: &AccessListData{}, + want: &jsonCall{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + data: &AccessListData{ + AccessList: AccessList{ + { + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }, + }, + }, + want: &jsonCall{ + AccessList: AccessList{ + { + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }, + }, + }, + wantJSON: `{ + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test toJSON + var j jsonCall + tt.data.toJSON(&j) + assert.Equal(t, tt.want.AccessList, j.AccessList) + + // Verify generated JSON string + jsonBytes, err := json.Marshal(j) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Test fromJSON + var data AccessListData + err = json.Unmarshal(jsonBytes, &j) + assert.NoError(t, err) + data.fromJSON(&j) + assert.Equal(t, tt.data.AccessList, data.AccessList) + }) + } +} + +func TestDynamicFeeData_JSON(t *testing.T) { + tests := []struct { + name string + data *DynamicFeeData + want *jsonCall + wantJSON string + }{ + { + name: "all fields nil", + data: &DynamicFeeData{}, + want: &jsonCall{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + data: &DynamicFeeData{ + MaxFeePerGas: big.NewInt(2000000000), + MaxPriorityFeePerGas: big.NewInt(1000000000), + }, + want: &jsonCall{ + MaxFeePerGas: NumberFromBigIntPtr(big.NewInt(2000000000)), + MaxPriorityFeePerGas: NumberFromBigIntPtr(big.NewInt(1000000000)), + }, + wantJSON: `{ + "maxFeePerGas": "0x77359400", + "maxPriorityFeePerGas": "0x3b9aca00" + }`, + }, + { + name: "zero values", + data: &DynamicFeeData{ + MaxFeePerGas: big.NewInt(0), + MaxPriorityFeePerGas: big.NewInt(0), + }, + want: &jsonCall{ + MaxFeePerGas: NumberFromBigIntPtr(big.NewInt(0)), + MaxPriorityFeePerGas: NumberFromBigIntPtr(big.NewInt(0)), + }, + wantJSON: `{ + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0" + }`, + }, + { + name: "max values", + data: &DynamicFeeData{ + MaxFeePerGas: big.NewInt(0).Sub(big.NewInt(0).SetBit(big.NewInt(0), 256, 1), big.NewInt(1)), + MaxPriorityFeePerGas: big.NewInt(0).Sub(big.NewInt(0).SetBit(big.NewInt(0), 256, 1), big.NewInt(1)), + }, + want: &jsonCall{ + MaxFeePerGas: NumberFromBigIntPtr(big.NewInt(0).Sub(big.NewInt(0).SetBit(big.NewInt(0), 256, 1), big.NewInt(1))), + MaxPriorityFeePerGas: NumberFromBigIntPtr(big.NewInt(0).Sub(big.NewInt(0).SetBit(big.NewInt(0), 256, 1), big.NewInt(1))), + }, + wantJSON: `{ + "maxFeePerGas": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "maxPriorityFeePerGas": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test toJSON + var j jsonCall + tt.data.toJSON(&j) + assert.Equal(t, tt.want.MaxFeePerGas, j.MaxFeePerGas) + assert.Equal(t, tt.want.MaxPriorityFeePerGas, j.MaxPriorityFeePerGas) + + // Verify generated JSON string + jsonBytes, err := json.Marshal(j) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Test fromJSON + var data DynamicFeeData + err = json.Unmarshal(jsonBytes, &j) + assert.NoError(t, err) + data.fromJSON(&j) + assert.Equal(t, tt.data.MaxFeePerGas, data.MaxFeePerGas) + assert.Equal(t, tt.data.MaxPriorityFeePerGas, data.MaxPriorityFeePerGas) + }) + } +} + +func TestBlobData_JSON(t *testing.T) { + remZerosRx := regexp.MustCompile(`0{128,}`) + tests := []struct { + name string + data *BlobData + want *jsonCall + wantJSON string + }{ + { + name: "all fields nil", + data: &BlobData{}, + want: &jsonCall{}, + wantJSON: `{}`, + }, + { + name: "blobs with sidecars", + data: &BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + { + Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone), + Sidecar: &BlobSidecar{ + Blob: kzg4844.Blob{0x01, 0x02, 0x03}, + Commitment: kzg4844.Commitment{0x04, 0x05, 0x06}, + Proof: kzg4844.Proof{0x07, 0x08, 0x09}, + }, + }, + }, + }, + want: &jsonCall{ + MaxFeePerBlobGas: NumberFromBigIntPtr(big.NewInt(3000000000)), + BlobHashes: []Hash{ + MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone), + }, + Blobs: []kzgBlob{ + {0x01, 0x02, 0x03}, + }, + Commitments: []kzgCommitment{ + {0x04, 0x05, 0x06}, + }, + Proofs: []kzgProof{ + {0x07, 0x08, 0x09}, + }, + }, + // Note, that "blobs" fields should be 128KiB in length, but to keep the test readable, + // repeated zeros are removed. + wantJSON: `{ + "maxFeePerBlobGas": "0xb2d05e00", + "blobVersionedHashes": ["0x6666666666666666666666666666666666666666666666666666666666666666"], + "blobs": ["0x010203"], + "commitments": ["0x040506000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"], + "proofs": ["0x070809000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"] + }`, + }, + { + name: "blobs without sidecars", + data: &BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + { + Hash: MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", PadNone), + Sidecar: nil, + }, + }, + }, + want: &jsonCall{ + MaxFeePerBlobGas: NumberFromBigIntPtr(big.NewInt(3000000000)), + BlobHashes: []Hash{ + MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", PadNone), + }, + }, + wantJSON: `{ + "maxFeePerBlobGas": "0xb2d05e00", + "blobVersionedHashes": ["0x7777777777777777777777777777777777777777777777777777777777777777"] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test toJSON + var j jsonCall + tt.data.toJSON(&j) + assert.Equal(t, tt.want.MaxFeePerBlobGas, j.MaxFeePerBlobGas) + assert.Equal(t, tt.want.BlobHashes, j.BlobHashes) + assert.Equal(t, tt.want.Blobs, j.Blobs) + assert.Equal(t, tt.want.Commitments, j.Commitments) + assert.Equal(t, tt.want.Proofs, j.Proofs) + + // Verify generated JSON string + jsonBytes, err := json.Marshal(j) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(remZerosRx.ReplaceAll(jsonBytes, []byte("")))) + + // Test fromJSON + var data BlobData + err = json.Unmarshal(jsonBytes, &j) + assert.NoError(t, err) + data.fromJSON(&j) + assert.Equal(t, tt.data.MaxFeePerBlobGas, data.MaxFeePerBlobGas) + assert.Equal(t, len(tt.data.Blobs), len(data.Blobs)) + for i := range tt.data.Blobs { + assert.Equal(t, tt.data.Blobs[i].Hash, data.Blobs[i].Hash) + if tt.data.Blobs[i].Sidecar == nil { + assert.Nil(t, data.Blobs[i].Sidecar) + } else { + assert.NotNil(t, data.Blobs[i].Sidecar) + assert.Equal(t, tt.data.Blobs[i].Sidecar.Blob, data.Blobs[i].Sidecar.Blob) + assert.Equal(t, tt.data.Blobs[i].Sidecar.Commitment, data.Blobs[i].Sidecar.Commitment) + assert.Equal(t, tt.data.Blobs[i].Sidecar.Proof, data.Blobs[i].Sidecar.Proof) + } + } + }) + } +} diff --git a/types/structs.go b/types/structs.go index 6ebcb73..c622f26 100644 --- a/types/structs.go +++ b/types/structs.go @@ -3,721 +3,26 @@ package types import ( "bytes" "encoding/json" + "errors" "fmt" "math/big" "time" "github.com/defiweb/go-rlp" -) - -// Call represents a call to a contract or a contract creation if To is nil. -type Call struct { - From *Address // From is the sender address. - To *Address // To is the recipient address. nil means contract creation. - GasLimit *uint64 // GasLimit is the gas limit, if 0, there is no limit. - GasPrice *big.Int // GasPrice is the gas price in wei per gas unit. - Value *big.Int // Value is the amount of wei to send. - Input []byte // Input is the input data. - - // EIP-2930 fields: - AccessList AccessList // AccessList is the list of addresses and storage keys that the transaction can access. - - // EIP-1559 fields: - MaxPriorityFeePerGas *big.Int // MaxPriorityFeePerGas is the maximum priority fee per gas the sender is willing to pay. - MaxFeePerGas *big.Int // MaxFeePerGas is the maximum fee per gas the sender is willing to pay. -} - -func NewCall() *Call { - return &Call{} -} - -func (c *Call) SetFrom(from Address) *Call { - c.From = &from - return c -} - -func (c *Call) SetTo(to Address) *Call { - c.To = &to - return c -} - -func (c *Call) SetGasLimit(gasLimit uint64) *Call { - c.GasLimit = &gasLimit - return c -} - -func (c *Call) SetGasPrice(gasPrice *big.Int) *Call { - c.GasPrice = gasPrice - return c -} - -func (c *Call) SetValue(value *big.Int) *Call { - c.Value = value - return c -} - -func (c *Call) SetInput(input []byte) *Call { - c.Input = input - return c -} - -func (c *Call) SetAccessList(accessList AccessList) *Call { - c.AccessList = accessList - return c -} - -func (c *Call) SetMaxPriorityFeePerGas(maxPriorityFeePerGas *big.Int) *Call { - c.MaxPriorityFeePerGas = maxPriorityFeePerGas - return c -} - -func (c *Call) SetMaxFeePerGas(maxFeePerGas *big.Int) *Call { - c.MaxFeePerGas = maxFeePerGas - return c -} - -func (c Call) Copy() *Call { - var ( - from *Address - to *Address - gasLimit *uint64 - gasPrice *big.Int - value *big.Int - input []byte - accessList AccessList - maxPriorityFeePerGas *big.Int - maxFeePerGas *big.Int - ) - if c.From != nil { - from = new(Address) - copy(from[:], c.From[:]) - } - if c.To != nil { - to = new(Address) - copy(to[:], c.To[:]) - } - if c.GasLimit != nil { - gasLimit = new(uint64) - *gasLimit = *c.GasLimit - } - if c.GasPrice != nil { - gasPrice = new(big.Int).Set(c.GasPrice) - } - if c.Value != nil { - value = new(big.Int).Set(c.Value) - } - if c.Input != nil { - input = make([]byte, len(c.Input)) - copy(input, c.Input) - } - if c.AccessList != nil { - accessList = c.AccessList.Copy() - } - if c.MaxPriorityFeePerGas != nil { - maxPriorityFeePerGas = new(big.Int).Set(c.MaxPriorityFeePerGas) - } - if c.MaxFeePerGas != nil { - maxFeePerGas = new(big.Int).Set(c.MaxFeePerGas) - } - return &Call{ - From: from, - To: to, - GasLimit: gasLimit, - GasPrice: gasPrice, - Value: value, - Input: input, - AccessList: accessList, - MaxPriorityFeePerGas: maxPriorityFeePerGas, - MaxFeePerGas: maxFeePerGas, - } -} - -func (c Call) MarshalJSON() ([]byte, error) { - call := &jsonCall{ - From: c.From, - To: c.To, - Data: c.Input, - AccessList: c.AccessList, - } - if c.GasLimit != nil { - call.GasLimit = NumberFromUint64Ptr(*c.GasLimit) - } - if c.GasPrice != nil { - call.GasPrice = NumberFromBigIntPtr(c.GasPrice) - } - if c.MaxFeePerGas != nil { - call.MaxFeePerGas = NumberFromBigIntPtr(c.MaxFeePerGas) - } - if c.MaxPriorityFeePerGas != nil { - call.MaxPriorityFeePerGas = NumberFromBigIntPtr(c.MaxPriorityFeePerGas) - } - if c.Value != nil { - value := NumberFromBigInt(c.Value) - call.Value = &value - } - return json.Marshal(call) -} - -func (c *Call) UnmarshalJSON(data []byte) error { - call := &jsonCall{} - if err := json.Unmarshal(data, call); err != nil { - return err - } - c.From = call.From - c.To = call.To - if call.GasLimit != nil { - gas := call.GasLimit.Big().Uint64() - c.GasLimit = &gas - } - if call.GasPrice != nil { - c.GasPrice = call.GasPrice.Big() - } - if call.MaxFeePerGas != nil { - c.MaxFeePerGas = call.MaxFeePerGas.Big() - } - if call.MaxPriorityFeePerGas != nil { - c.MaxPriorityFeePerGas = call.MaxPriorityFeePerGas.Big() - } - if call.Value != nil { - c.Value = call.Value.Big() - } - c.Input = call.Data - c.AccessList = call.AccessList - return nil -} -type jsonCall struct { - From *Address `json:"from,omitempty"` - To *Address `json:"to,omitempty"` - GasLimit *Number `json:"gas,omitempty"` - GasPrice *Number `json:"gasPrice,omitempty"` - MaxFeePerGas *Number `json:"maxFeePerGas,omitempty"` - MaxPriorityFeePerGas *Number `json:"maxPriorityFeePerGas,omitempty"` - Value *Number `json:"value,omitempty"` - Data Bytes `json:"data,omitempty"` - AccessList AccessList `json:"accessList,omitempty"` -} - -// TransactionType is the type of transaction. -type TransactionType uint64 - -// Transaction types. -const ( - LegacyTxType TransactionType = iota - AccessListTxType - DynamicFeeTxType + "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/kzg4844" ) -// Transaction represents a transaction. -type Transaction struct { - Call - - // Transaction data: - Type TransactionType // Type is the transaction type. - Nonce *uint64 // Nonce is the number of transactions made by the sender prior to this one. - Signature *Signature // Signature of the transaction. - - // EIP-2930 fields: - ChainID *uint64 // ChainID is the chain ID of the transaction. -} - -func NewTransaction() *Transaction { - return &Transaction{} -} - -func (t *Transaction) SetFrom(from Address) *Transaction { - t.From = &from - return t -} - -func (t *Transaction) SetTo(to Address) *Transaction { - t.To = &to - return t -} - -func (t *Transaction) SetGasLimit(gasLimit uint64) *Transaction { - t.GasLimit = &gasLimit - return t -} - -func (t *Transaction) SetGasPrice(gasPrice *big.Int) *Transaction { - t.GasPrice = gasPrice - return t -} - -func (t *Transaction) SetValue(value *big.Int) *Transaction { - t.Value = value - return t -} - -func (t *Transaction) SetInput(input []byte) *Transaction { - t.Input = input - return t -} - -func (t *Transaction) SetAccessList(accessList AccessList) *Transaction { - t.AccessList = accessList - return t -} - -func (t *Transaction) SetMaxPriorityFeePerGas(maxPriorityFeePerGas *big.Int) *Transaction { - t.MaxPriorityFeePerGas = maxPriorityFeePerGas - return t -} - -func (t *Transaction) SetMaxFeePerGas(maxFeePerGas *big.Int) *Transaction { - t.MaxFeePerGas = maxFeePerGas - return t -} - -func (t *Transaction) SetType(transactionType TransactionType) *Transaction { - t.Type = transactionType - return t -} - -func (t *Transaction) SetNonce(nonce uint64) *Transaction { - t.Nonce = &nonce - return t -} - -func (t *Transaction) SetSignature(signature Signature) *Transaction { - t.Signature = &signature - return t -} - -func (t *Transaction) SetChainID(chainID uint64) *Transaction { - t.ChainID = &chainID - return t -} - -// Raw returns the raw transaction data that could be sent to the network. -func (t Transaction) Raw() ([]byte, error) { - return t.EncodeRLP() -} - -func (t *Transaction) Copy() *Transaction { - var ( - nonce *uint64 - signature *Signature - chainID *uint64 - ) - if t.Nonce != nil { - nonce = new(uint64) - *nonce = *t.Nonce - } - if t.Signature != nil { - signature = t.Signature.Copy() - } - if t.ChainID != nil { - chainID = new(uint64) - *chainID = *t.ChainID - } - return &Transaction{ - Call: *t.Call.Copy(), - Type: t.Type, - Nonce: nonce, - Signature: signature, - ChainID: chainID, - } -} - -func (t Transaction) MarshalJSON() ([]byte, error) { - transaction := &jsonTransaction{} - transaction.To = t.To - transaction.From = t.From - if t.GasLimit != nil { - transaction.GasLimit = NumberFromUint64Ptr(*t.GasLimit) - } - if t.GasPrice != nil { - transaction.GasPrice = NumberFromBigIntPtr(t.GasPrice) - } - if t.MaxFeePerGas != nil { - transaction.MaxFeePerGas = NumberFromBigIntPtr(t.MaxFeePerGas) - } - if t.MaxPriorityFeePerGas != nil { - transaction.MaxPriorityFeePerGas = NumberFromBigIntPtr(t.MaxPriorityFeePerGas) - } - transaction.Input = t.Input - if t.Nonce != nil { - transaction.Nonce = NumberFromUint64Ptr(*t.Nonce) - } - if t.Value != nil { - transaction.Value = NumberFromBigIntPtr(t.Value) - } - transaction.AccessList = t.AccessList - if t.Signature != nil { - transaction.V = NumberFromBigIntPtr(t.Signature.V) - transaction.R = NumberFromBigIntPtr(t.Signature.R) - transaction.S = NumberFromBigIntPtr(t.Signature.S) - } - return json.Marshal(transaction) -} - -func (t *Transaction) UnmarshalJSON(data []byte) error { - transaction := &jsonTransaction{} - if err := json.Unmarshal(data, transaction); err != nil { - return err - } - t.To = transaction.To - t.From = transaction.From - if transaction.GasLimit != nil { - gas := transaction.GasLimit.Big().Uint64() - t.GasLimit = &gas - } - if transaction.GasPrice != nil { - t.GasPrice = transaction.GasPrice.Big() - } - if transaction.MaxFeePerGas != nil { - t.MaxFeePerGas = transaction.MaxFeePerGas.Big() - } - if transaction.MaxPriorityFeePerGas != nil { - t.MaxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Big() - } - t.Input = transaction.Input - if transaction.Nonce != nil { - nonce := transaction.Nonce.Big().Uint64() - t.Nonce = &nonce - } - if transaction.Value != nil { - t.Value = transaction.Value.Big() - } - t.AccessList = transaction.AccessList - if transaction.V != nil && transaction.R != nil && transaction.S != nil { - t.Signature = SignatureFromVRSPtr(transaction.V.Big(), transaction.R.Big(), transaction.S.Big()) - } - return nil -} - -//nolint:funlen -func (t Transaction) EncodeRLP() ([]byte, error) { - var ( - chainID = uint64(1) - nonce = uint64(0) - gasPrice = big.NewInt(0) - gasLimit = uint64(0) - maxPriorityFeePerGas = big.NewInt(0) - maxFeePerGas = big.NewInt(0) - to = ([]byte)(nil) - value = big.NewInt(0) - accessList = (AccessList)(nil) - v = big.NewInt(0) - r = big.NewInt(0) - s = big.NewInt(0) - ) - if t.ChainID != nil { - chainID = *t.ChainID - } - if t.Nonce != nil { - nonce = *t.Nonce - } - if t.GasPrice != nil { - gasPrice = t.GasPrice - } - if t.GasLimit != nil { - gasLimit = *t.GasLimit - } - if t.MaxPriorityFeePerGas != nil { - maxPriorityFeePerGas = t.MaxPriorityFeePerGas - } - if t.MaxFeePerGas != nil { - maxFeePerGas = t.MaxFeePerGas - } - if t.To != nil { - to = t.To[:] - } - if t.Value != nil { - value = t.Value - } - if t.AccessList != nil { - accessList = t.AccessList - } - if t.Signature != nil { - v = t.Signature.V - r = t.Signature.R - s = t.Signature.S - } - switch t.Type { - case LegacyTxType: - return rlp.NewList( - rlp.NewUint(nonce), - rlp.NewBigInt(gasPrice), - rlp.NewUint(gasLimit), - rlp.NewBytes(to), - rlp.NewBigInt(value), - rlp.NewBytes(t.Input), - rlp.NewBigInt(v), - rlp.NewBigInt(r), - rlp.NewBigInt(s), - ).EncodeRLP() - case AccessListTxType: - bin, err := rlp.NewList( - rlp.NewUint(chainID), - rlp.NewUint(nonce), - rlp.NewBigInt(gasPrice), - rlp.NewUint(gasLimit), - rlp.NewBytes(to), - rlp.NewBigInt(value), - rlp.NewBytes(t.Input), - &t.AccessList, - rlp.NewBigInt(v), - rlp.NewBigInt(r), - rlp.NewBigInt(s), - ).EncodeRLP() - if err != nil { - return nil, err - } - return append([]byte{byte(t.Type)}, bin...), nil - case DynamicFeeTxType: - bin, err := rlp.NewList( - rlp.NewUint(chainID), - rlp.NewUint(nonce), - rlp.NewBigInt(maxPriorityFeePerGas), - rlp.NewBigInt(maxFeePerGas), - rlp.NewUint(gasLimit), - rlp.NewBytes(to), - rlp.NewBigInt(value), - rlp.NewBytes(t.Input), - &accessList, - rlp.NewBigInt(v), - rlp.NewBigInt(r), - rlp.NewBigInt(s), - ).EncodeRLP() - if err != nil { - return nil, err - } - return append([]byte{byte(t.Type)}, bin...), nil - default: - return nil, fmt.Errorf("unknown transaction type: %d", t.Type) - } -} - -//nolint:funlen -func (t *Transaction) DecodeRLP(data []byte) (int, error) { - if len(data) == 0 { - return 0, fmt.Errorf("empty data") - } - var ( - list *rlp.ListItem - chainID = &rlp.UintItem{} - nonce = &rlp.UintItem{} - gasPrice = &rlp.BigIntItem{} - gasLimit = &rlp.UintItem{} - maxPriorityFeePerGas = &rlp.BigIntItem{} - maxFeePerGas = &rlp.BigIntItem{} - to = &rlp.StringItem{} - value = &rlp.BigIntItem{} - input = &rlp.StringItem{} - accessList = &AccessList{} - v = &rlp.BigIntItem{} - r = &rlp.BigIntItem{} - s = &rlp.BigIntItem{} - ) - switch { - case data[0] >= 0x80: // LegacyTxType - t.Type = LegacyTxType - list = rlp.NewList( - nonce, - gasPrice, - gasLimit, - to, - value, - input, - v, - r, - s, - ) - case data[0] == byte(AccessListTxType): - t.Type = AccessListTxType - data = data[1:] - list = rlp.NewList( - chainID, - nonce, - gasPrice, - gasLimit, - to, - value, - input, - accessList, - v, - r, - s, - ) - case data[0] == byte(DynamicFeeTxType): - t.Type = DynamicFeeTxType - data = data[1:] - list = rlp.NewList( - chainID, - nonce, - maxPriorityFeePerGas, - maxFeePerGas, - gasLimit, - to, - value, - input, - accessList, - v, - r, - s, - ) - default: - return 0, fmt.Errorf("invalid transaction type: %d", data[0]) - } - if _, err := rlp.DecodeTo(data, list); err != nil { - return 0, err - } - t.ChainID = &chainID.X - t.Nonce = &nonce.X - t.GasPrice = gasPrice.X - t.GasLimit = &gasLimit.X - t.MaxPriorityFeePerGas = maxPriorityFeePerGas.X - t.MaxFeePerGas = maxFeePerGas.X - t.To = AddressFromBytesPtr(to.Bytes()) - t.Value = value.X - if len(input.Bytes()) > 0 { - t.Input = input.Bytes() - } - if len(*accessList) > 0 { - t.AccessList = *accessList - } - if v.X.Sign() != 0 || r.X.Sign() != 0 || s.X.Sign() != 0 { - t.Signature = &Signature{ - V: v.X, - R: r.X, - S: s.X, - } - } - return len(data), nil -} - -// Hash returns the hash of the transaction (transaction ID). -func (t Transaction) Hash(h HashFunc) (Hash, error) { - raw, err := t.Raw() - if err != nil { - return Hash{}, err - } - return h(raw), nil -} - -type jsonTransaction struct { - From *Address `json:"from,omitempty"` - To *Address `json:"to,omitempty"` - GasLimit *Number `json:"gas,omitempty"` - GasPrice *Number `json:"gasPrice,omitempty"` - MaxFeePerGas *Number `json:"maxFeePerGas,omitempty"` - MaxPriorityFeePerGas *Number `json:"maxPriorityFeePerGas,omitempty"` - Input Bytes `json:"input,omitempty"` - Nonce *Number `json:"nonce,omitempty"` - Value *Number `json:"value,omitempty"` - AccessList AccessList `json:"accessList,omitempty"` - V *Number `json:"v,omitempty"` - R *Number `json:"r,omitempty"` - S *Number `json:"s,omitempty"` -} - -// OnChainTransaction represents a transaction that is included in a block. -type OnChainTransaction struct { - Transaction - - // On-chain fields, only available when the transaction is included in a block: - Hash *Hash // Hash of the transaction. - BlockHash *Hash // BlockHash is the hash of the block where this transaction was in. - BlockNumber *big.Int // BlockNumber is the block number where this transaction was in. - TransactionIndex *uint64 // TransactionIndex is the index of the transaction in the block. -} - -type jsonOnChainTransaction struct { - jsonTransaction - Hash *Hash `json:"hash,omitempty"` - BlockHash *Hash `json:"blockHash,omitempty"` - BlockNumber *Number `json:"blockNumber,omitempty"` - TransactionIndex *Number `json:"transactionIndex,omitempty"` -} - -func (t OnChainTransaction) MarshalJSON() ([]byte, error) { - transaction := &jsonOnChainTransaction{} - transaction.To = t.To - transaction.From = t.From - if t.GasLimit != nil { - transaction.GasLimit = NumberFromUint64Ptr(*t.GasLimit) - } - if t.GasPrice != nil { - transaction.GasPrice = NumberFromBigIntPtr(t.GasPrice) - } - if t.MaxFeePerGas != nil { - transaction.MaxFeePerGas = NumberFromBigIntPtr(t.MaxFeePerGas) - } - if t.MaxPriorityFeePerGas != nil { - transaction.MaxPriorityFeePerGas = NumberFromBigIntPtr(t.MaxPriorityFeePerGas) - } - transaction.Input = t.Input - if t.Nonce != nil { - transaction.Nonce = NumberFromUint64Ptr(*t.Nonce) - } - if t.Value != nil { - transaction.Value = NumberFromBigIntPtr(t.Value) - } - transaction.AccessList = t.AccessList - if t.Signature != nil { - transaction.V = NumberFromBigIntPtr(t.Signature.V) - transaction.R = NumberFromBigIntPtr(t.Signature.R) - transaction.S = NumberFromBigIntPtr(t.Signature.S) - } - transaction.Hash = t.Hash - transaction.BlockHash = t.BlockHash - if t.BlockNumber != nil { - transaction.BlockNumber = NumberFromBigIntPtr(t.BlockNumber) - } - if t.TransactionIndex != nil { - transaction.TransactionIndex = NumberFromUint64Ptr(*t.TransactionIndex) - } - return json.Marshal(transaction) -} - -func (t *OnChainTransaction) UnmarshalJSON(data []byte) error { - transaction := &jsonOnChainTransaction{} - if err := json.Unmarshal(data, transaction); err != nil { - return err - } - t.To = transaction.To - t.From = transaction.From - if transaction.GasLimit != nil { - gas := transaction.GasLimit.Big().Uint64() - t.GasLimit = &gas - } - if transaction.GasPrice != nil { - t.GasPrice = transaction.GasPrice.Big() - } - if transaction.MaxFeePerGas != nil { - t.MaxFeePerGas = transaction.MaxFeePerGas.Big() - } - if transaction.MaxPriorityFeePerGas != nil { - t.MaxPriorityFeePerGas = transaction.MaxPriorityFeePerGas.Big() - } - t.Input = transaction.Input - if transaction.Nonce != nil { - nonce := transaction.Nonce.Big().Uint64() - t.Nonce = &nonce - } - if transaction.Value != nil { - t.Value = transaction.Value.Big() - } - t.AccessList = transaction.AccessList - if transaction.V != nil && transaction.R != nil && transaction.S != nil { - t.Signature = SignatureFromVRSPtr(transaction.V.Big(), transaction.R.Big(), transaction.S.Big()) - } - t.Hash = transaction.Hash - t.BlockHash = transaction.BlockHash - if transaction.BlockNumber != nil { - t.BlockNumber = transaction.BlockNumber.Big() - } - if transaction.TransactionIndex != nil { - index := transaction.TransactionIndex.Big().Uint64() - t.TransactionIndex = &index - } - return nil -} - -// AccessList is an EIP-2930 access list. +// AccessList represents an Ethereum access list as defined in EIP-2930. +// +// EIP-2930 introduced a new transaction type that includes an optional +// access list, which specifies a list of addresses and storage keys that the +// transaction plans to access. By declaring these accesses upfront, +// transactions can benefit from reduced gas costs for cold accesses, as +// the specified addresses and storage slots are warmed up ahead of execution. +// +// https://eips.ethereum.org/EIPS/eip-2930 type AccessList []AccessTuple // AccessTuple is the element type of access list. @@ -726,6 +31,7 @@ type AccessTuple struct { StorageKeys []Hash `json:"storageKeys"` } +// Copy creates a deep copy of the access list. func (a *AccessList) Copy() AccessList { if a == nil { return nil @@ -737,27 +43,29 @@ func (a *AccessList) Copy() AccessList { return c } +// EncodeRLP implements the rlp.Encoder interface. func (a AccessList) EncodeRLP() ([]byte, error) { - l := rlp.NewList() + l := rlp.List{} for _, tuple := range a { - tuple := tuple // Copy value because of loop variable reuse. - l.Append(&tuple) + tuple := tuple + l.Add(&tuple) } return rlp.Encode(l) } +// DecodeRLP implements the rlp.Decoder interface. func (a *AccessList) DecodeRLP(data []byte) (int, error) { - d, n, err := rlp.Decode(data) + d, n, err := rlp.DecodeLazy(data) if err != nil { return 0, err } - l, err := d.GetList() + l, err := d.List() if err != nil { return 0, err } for _, tuple := range l { var t AccessTuple - if err := tuple.DecodeTo(&t); err != nil { + if err := tuple.Decode(&t); err != nil { return 0, err } *a = append(*a, t) @@ -765,6 +73,7 @@ func (a *AccessList) DecodeRLP(data []byte) (int, error) { return n, nil } +// Copy creates a deep copy of the access tuple. func (a *AccessTuple) Copy() AccessTuple { keys := make([]Hash, len(a.StorageKeys)) copy(keys, a.StorageKeys) @@ -774,37 +83,39 @@ func (a *AccessTuple) Copy() AccessTuple { } } +// EncodeRLP implements the rlp.Encoder interface. func (a AccessTuple) EncodeRLP() ([]byte, error) { - h := rlp.NewList() + h := rlp.List{} for _, hash := range a.StorageKeys { hash := hash - h.Append(&hash) + h.Add(&hash) } - return rlp.Encode(rlp.NewList(&a.Address, h)) + return rlp.Encode(rlp.List{a.Address, h}) } +// DecodeRLP implements the rlp.Decoder interface. func (a *AccessTuple) DecodeRLP(data []byte) (int, error) { - d, n, err := rlp.Decode(data) + d, n, err := rlp.DecodeLazy(data) if err != nil { return n, err } - l, err := d.GetList() + l, err := d.List() if err != nil { return n, err } if len(l) != 2 { return n, fmt.Errorf("invalid access list tuple") } - if err := l[0].DecodeTo(&a.Address); err != nil { + if err := l[0].Decode(&a.Address); err != nil { return n, err } - h, err := l[1].GetList() + h, err := l[1].List() if err != nil { return n, err } for _, item := range h { var hash Hash - if err := item.DecodeTo(&hash); err != nil { + if err := item.Decode(&hash); err != nil { return n, err } a.StorageKeys = append(a.StorageKeys, hash) @@ -812,6 +123,125 @@ func (a *AccessTuple) DecodeRLP(data []byte) (int, error) { return n, nil } +// BlobInfo represents the information of an EIP-4844 blob carried in a +// transaction. +// +// EIP-4844 introduces "blob-carrying transactions" to Ethereum, which include +// a new type of data called "blobs". These blobs are large binary objects that +// are not directly accessible by the EVM but are committed to the consensus +// layer. +// +// https://eips.ethereum.org/EIPS/eip-4844 +type BlobInfo struct { + Hash Hash // Hash of the blob. + Sidecar *BlobSidecar // Optional sidecar containing blob components. +} + +// BlobSidecar contains the components of the blob stored by the consensus +// layer. +type BlobSidecar struct { + Blob kzg4844.Blob // The actual blob data. + Commitment kzg4844.Commitment // Commitment for the blob. + Proof kzg4844.Proof // Proof for the blob. +} + +// ComputeHash computes the blob hash of the given blob sidecar. +func (sc *BlobSidecar) ComputeHash() Hash { + return crypto.KZGComputeBlobHashV1(sc.Commitment) +} + +// NewBlobInfo creates a new EIP-4844 BlobInfo from the given blob, computing +// its hash, commitment, and proof. +// +// The provided blob must not be nil and must be a valid EIP-4844 blob +// of length 131072 bytes (4096 field elements of 32 bytes each). +// Each field element is a 32-byte big-endian integer not exceeding the +// BLS12-381 field modulus specified in EIP-4844. +// +// NewBlobInfo does not perform any encoding on the provided data. +// +// Returns an error if the blob is nil or if the commitment/proof computation +// fails. +func NewBlobInfo(b *kzg4844.Blob) (BlobInfo, error) { + if b == nil { + return BlobInfo{}, errors.New("blob is nil") + } + c, err := crypto.KZGBlobToCommitment(b) + if err != nil { + return BlobInfo{}, err + } + p, err := crypto.KZGComputeBlobProof(b, c) + if err != nil { + return BlobInfo{}, err + } + s := &BlobSidecar{ + Blob: *b, + Commitment: c, + Proof: p, + } + return BlobInfo{ + Hash: s.ComputeHash(), + Sidecar: s, + }, nil +} + +// TransactionOnChain represents a transaction on the blockchain. +type TransactionOnChain struct { + Decoder JSONTransactionDecoder // Decoder is an optional transaction decoder, if nil, the default decoder is used. + Transaction Transaction // Transaction is the transaction data. + Hash *Hash // Hash of the transaction. + BlockHash *Hash // BlockHash is the hash of the block where this transaction was in. + BlockNumber *big.Int // BlockNumber is the block number where this transaction was in. + TransactionIndex *uint64 // TransactionIndex is the index of the transaction in the block. +} + +// MarshalJSON implements the json.Marshaler interface. +func (t *TransactionOnChain) MarshalJSON() ([]byte, error) { + ocd := &jsonOnChainTransaction{} + ocd.Hash = t.Hash + ocd.BlockHash = t.BlockHash + ocd.BlockNumber = NumberFromBigIntPtr(t.BlockNumber) + if t.TransactionIndex != nil { + ocd.TransactionIndex = NumberFromUint64Ptr(*t.TransactionIndex) + } + return marshalJSONMerge( + t.Transaction, + ocd, + ) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *TransactionOnChain) UnmarshalJSON(data []byte) error { + ocd := &jsonOnChainTransaction{} + if err := json.Unmarshal(data, ocd); err != nil { + return err + } + t.Hash = ocd.Hash + t.BlockHash = ocd.BlockHash + t.BlockNumber = ocd.BlockNumber.Big() + if ocd.TransactionIndex != nil { + index := ocd.TransactionIndex.Big().Uint64() + t.TransactionIndex = &index + } + dec := t.Decoder + if dec == nil { + dec = DefaultTransactionDecoder + } + tx, err := dec.DecodeJSON(data) + if err != nil { + return err + } + t.Transaction = tx + return nil +} + +type jsonOnChainTransaction struct { + Hash *Hash `json:"hash,omitempty"` + BlockHash *Hash `json:"blockHash,omitempty"` + BlockNumber *Number `json:"blockNumber,omitempty"` + TransactionIndex *Number `json:"transactionIndex,omitempty"` +} + // TransactionReceipt represents transaction receipt. type TransactionReceipt struct { TransactionHash Hash // TransactionHash is the hash of the transaction. @@ -830,6 +260,7 @@ type TransactionReceipt struct { Status *uint64 // Status is the status of the transaction. } +// MarshalJSON implements the json.Marshaler interface. func (t TransactionReceipt) MarshalJSON() ([]byte, error) { receipt := &jsonTransactionReceipt{ TransactionHash: t.TransactionHash, @@ -853,6 +284,7 @@ func (t TransactionReceipt) MarshalJSON() ([]byte, error) { return json.Marshal(receipt) } +// UnmarshalJSON implements the json.Unmarshaler interface. func (t *TransactionReceipt) UnmarshalJSON(data []byte) error { receipt := &jsonTransactionReceipt{} if err := json.Unmarshal(data, receipt); err != nil { @@ -895,6 +327,7 @@ type jsonTransactionReceipt struct { Status *Number `json:"status"` } +// Block represents a block on the blockchain. type Block struct { Number *big.Int // Block is the block number. Hash Hash // Hash is the hash of the block. @@ -914,11 +347,12 @@ type Block struct { GasUsed uint64 // GasUsed is the total used gas by all transactions in this block. Timestamp time.Time // Timestamp is the time at which the block was collated. Uncles []Hash // Uncles is the list of uncle hashes. - Transactions []OnChainTransaction // Transactions is the list of transactions in the block. + Transactions []TransactionOnChain // Transactions is the list of transactions in the block. TransactionHashes []Hash // TransactionHashes is the list of transaction hashes in the block. ExtraData []byte // ExtraData is the "extra data" field of this block. } +// MarshalJSON implements the json.Marshaler interface. func (b Block) MarshalJSON() ([]byte, error) { block := &jsonBlock{ Number: NumberFromBigInt(b.Number), @@ -950,6 +384,7 @@ func (b Block) MarshalJSON() ([]byte, error) { return json.Marshal(block) } +// UnmarshalJSON implements the json.Unmarshaler interface. func (b *Block) UnmarshalJSON(data []byte) error { block := &jsonBlock{} if err := json.Unmarshal(data, block); err != nil { @@ -988,9 +423,9 @@ type jsonBlock struct { TransactionsRoot Hash `json:"transactionsRoot"` MixHash Hash `json:"mixHash"` Sha3Uncles Hash `json:"sha3Uncles"` - Nonce hexNonce `json:"nonce"` + Nonce nonce `json:"nonce"` Miner Address `json:"miner"` - LogsBloom hexBloom `json:"logsBloom"` + LogsBloom bloom `json:"logsBloom"` Difficulty Number `json:"difficulty"` TotalDifficulty Number `json:"totalDifficulty"` Size Number `json:"size"` @@ -1003,7 +438,7 @@ type jsonBlock struct { } type jsonBlockTransactions struct { - Objects []OnChainTransaction + Objects []TransactionOnChain Hashes []Hash } @@ -1024,7 +459,8 @@ func (b *jsonBlockTransactions) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &b.Hashes) } -// FeeHistory represents the result of the feeHistory Client call. +// FeeHistory contains information about the fee structure and gas usage +// over a range of blocks. type FeeHistory struct { OldestBlock uint64 // OldestBlock is the oldest block number for which the base fee and gas used are returned. Reward [][]*big.Int // Reward is the reward for each block in the range [OldestBlock, LatestBlock]. @@ -1032,6 +468,7 @@ type FeeHistory struct { GasUsedRatio []float64 // GasUsedRatio is the gas used ratio for each block in the range [OldestBlock, LatestBlock]. } +// MarshalJSON implements the json.Marshaler interface. func (f FeeHistory) MarshalJSON() ([]byte, error) { feeHistory := &jsonFeeHistory{ OldestBlock: NumberFromUint64(f.OldestBlock), @@ -1055,6 +492,7 @@ func (f FeeHistory) MarshalJSON() ([]byte, error) { return json.Marshal(feeHistory) } +// UnmarshalJSON implements the json.Unmarshaler interface. func (f *FeeHistory) UnmarshalJSON(input []byte) error { feeHistory := &jsonFeeHistory{} if err := json.Unmarshal(input, feeHistory); err != nil { @@ -1097,6 +535,7 @@ type Log struct { Removed bool // Removed is true if the log was reverted due to a chain reorganization. False if unknown. } +// MarshalJSON implements the json.Marshaler interface. func (l Log) MarshalJSON() ([]byte, error) { j := &jsonLog{} j.Address = l.Address @@ -1117,6 +556,7 @@ func (l Log) MarshalJSON() ([]byte, error) { return json.Marshal(j) } +// UnmarshalJSON implements the json.Unmarshaler interface. func (l *Log) UnmarshalJSON(input []byte) error { log := &jsonLog{} if err := json.Unmarshal(input, log); err != nil { @@ -1163,45 +603,47 @@ type FilterLogsQuery struct { BlockHash *Hash } +// NewFilterLogsQuery creates a new FilterLogsQuery. func NewFilterLogsQuery() *FilterLogsQuery { return &FilterLogsQuery{} } -func (q *FilterLogsQuery) SetAddresses(addresses ...Address) *FilterLogsQuery { +// SetAddresses sets the addresses to filter logs. +func (q *FilterLogsQuery) SetAddresses(addresses ...Address) { q.Address = addresses - return q } -func (q *FilterLogsQuery) AddAddresses(addresses ...Address) *FilterLogsQuery { +// AddAddresses adds addresses to filter logs. +func (q *FilterLogsQuery) AddAddresses(addresses ...Address) { q.Address = append(q.Address, addresses...) - return q } -func (q *FilterLogsQuery) SetFromBlock(fromBlock *BlockNumber) *FilterLogsQuery { +// SetFromBlock sets the starting block number to filter logs. +func (q *FilterLogsQuery) SetFromBlock(fromBlock *BlockNumber) { q.FromBlock = fromBlock - return q } -func (q *FilterLogsQuery) SetToBlock(toBlock *BlockNumber) *FilterLogsQuery { +// SetToBlock sets the ending block number to filter logs. +func (q *FilterLogsQuery) SetToBlock(toBlock *BlockNumber) { q.ToBlock = toBlock - return q } -func (q *FilterLogsQuery) SetTopics(topics ...[]Hash) *FilterLogsQuery { +// SetTopics sets the topics to filter logs. +func (q *FilterLogsQuery) SetTopics(topics ...[]Hash) { q.Topics = topics - return q } -func (q *FilterLogsQuery) AddTopics(topics ...[]Hash) *FilterLogsQuery { +// AddTopics adds topics to filter logs. +func (q *FilterLogsQuery) AddTopics(topics ...[]Hash) { q.Topics = append(q.Topics, topics...) - return q } -func (q *FilterLogsQuery) SetBlockHash(blockHash *Hash) *FilterLogsQuery { +// SetBlockHash sets the block hash to filter logs. +func (q *FilterLogsQuery) SetBlockHash(blockHash *Hash) { q.BlockHash = blockHash - return q } +// MarshalJSON implements the json.Marshaler interface. func (q FilterLogsQuery) MarshalJSON() ([]byte, error) { logsQuery := &jsonFilterLogsQuery{ FromBlock: q.FromBlock, @@ -1213,7 +655,7 @@ func (q FilterLogsQuery) MarshalJSON() ([]byte, error) { copy(logsQuery.Address, q.Address) } if len(q.Topics) > 0 { - logsQuery.Topics = make([]hashList, len(q.Topics)) + logsQuery.Topics = make([]oneOrList[Hash], len(q.Topics)) for i, t := range q.Topics { logsQuery.Topics[i] = make([]Hash, len(t)) copy(logsQuery.Topics[i], t) @@ -1222,6 +664,7 @@ func (q FilterLogsQuery) MarshalJSON() ([]byte, error) { return json.Marshal(logsQuery) } +// UnmarshalJSON implements the json.Unmarshaler interface. func (q *FilterLogsQuery) UnmarshalJSON(input []byte) error { logsQuery := &jsonFilterLogsQuery{} if err := json.Unmarshal(input, logsQuery); err != nil { @@ -1245,9 +688,16 @@ func (q *FilterLogsQuery) UnmarshalJSON(input []byte) error { } type jsonFilterLogsQuery struct { - Address addressList `json:"address"` - FromBlock *BlockNumber `json:"fromBlock,omitempty"` - ToBlock *BlockNumber `json:"toBlock,omitempty"` - Topics []hashList `json:"topics"` - BlockHash *Hash `json:"blockhash,omitempty"` + Address oneOrList[Address] `json:"address"` + FromBlock *BlockNumber `json:"fromBlock,omitempty"` + ToBlock *BlockNumber `json:"toBlock,omitempty"` + Topics []oneOrList[Hash] `json:"topics"` + BlockHash *Hash `json:"blockhash,omitempty"` +} + +// SyncStatus represents the sync status of a node. +type SyncStatus struct { + StartingBlock BlockNumber `json:"startingBlock"` + CurrentBlock BlockNumber `json:"currentBlock"` + HighestBlock BlockNumber `json:"highestBlock"` } diff --git a/types/structs_test.go b/types/structs_test.go index 112020b..b93d9fe 100644 --- a/types/structs_test.go +++ b/types/structs_test.go @@ -7,162 +7,67 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/defiweb/go-eth/hexutil" ) -//nolint:funlen -func TestTransaction_RLP(t1 *testing.T) { +func TestTransactionOnChain_JSON(t *testing.T) { tests := []struct { - tx *Transaction - want []byte + tx *TransactionOnChain + wantJSON string }{ - // Empty transaction: - { - tx: (&Transaction{}). - SetGasLimit(0). - SetGasPrice(big.NewInt(0)). - SetNonce(0). - SetValue(big.NewInt(0)), - want: hexutil.MustHexToBytes("c9808080808080808080"), - }, - // Legacy transaction: - { - tx: (&Transaction{}). - SetType(LegacyTxType). - SetFrom(MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetGasPrice(big.NewInt(1000000000)). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")), - want: hexutil.MustHexToBytes("f87001843b9aca00830186a0942222222222222222222222222222222222222222880de0b6b3a764000084010203046fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84"), - }, - // Access list transaction: { - tx: (&Transaction{}). - SetType(AccessListTxType). - SetFrom(MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetGasPrice(big.NewInt(1000000000)). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1). - SetAccessList(AccessList{ - AccessTuple{ - Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), - StorageKeys: []Hash{ - MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), - MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + wantJSON: ` + { + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "gasPrice": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "blockNumber": "0x3", + "transactionIndex": "0x4" + } + `, + tx: &TransactionOnChain{ + Transaction: &TransactionLegacy{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, }, - }}), - want: hexutil.MustHexToBytes("01f8ce0101843b9aca00830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a055555555555555555555555555555555555555555555555555555555555555556fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84"), - }, - // Dynamic fee transaction: - { - tx: (&Transaction{}). - SetType(DynamicFeeTxType). - SetFrom(MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1). - SetMaxPriorityFeePerGas(big.NewInt(1000000000)). - SetMaxFeePerGas(big.NewInt(2000000000)). - SetAccessList(AccessList{ - AccessTuple{ - Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), - StorageKeys: []Hash{ - MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), - MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), }, }, - }), - want: hexutil.MustHexToBytes("02f8d30101843b9aca008477359400830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a055555555555555555555555555555555555555555555555555555555555555556fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84"), - }, - // Dynamic fee transaction with no access list: - { - tx: (&Transaction{}). - SetType(DynamicFeeTxType). - SetFrom(MustAddressFromHex("0x1111111111111111111111111111111111111111")). - SetTo(MustAddressFromHex("0x2222222222222222222222222222222222222222")). - SetGasLimit(100000). - SetInput([]byte{1, 2, 3, 4}). - SetNonce(1). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(MustSignatureFromHex("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f")). - SetChainID(1). - SetMaxPriorityFeePerGas(big.NewInt(1000000000)). - SetMaxFeePerGas(big.NewInt(2000000000)), - want: hexutil.MustHexToBytes("02f8770101843b9aca008477359400830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304c06fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84"), - }, - // Example from EIP-155: - { - tx: (&Transaction{}). - SetType(LegacyTxType). - SetChainID(1). - SetTo(MustAddressFromHex("0x3535353535353535353535353535353535353535")). - SetGasLimit(21000). - SetGasPrice(big.NewInt(20000000000)). - SetNonce(9). - SetValue(big.NewInt(1000000000000000000)). - SetSignature(SignatureFromVRS( - func() *big.Int { - v, _ := new(big.Int).SetString("37", 10) - return v - }(), - func() *big.Int { - v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) - return v - }(), - func() *big.Int { - v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) - return v - }(), - )), - want: hexutil.MustHexToBytes("f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"), + }, + Hash: MustHashFromHexPtr("0x1111111111111111111111111111111111111111111111111111111111111111", PadNone), + BlockHash: MustHashFromHexPtr("0x2222222222222222222222222222222222222222222222222222222222222222", PadNone), + BlockNumber: big.NewInt(3), + TransactionIndex: ptr(uint64(4)), + }, }, } for n, tt := range tests { - t1.Run(fmt.Sprintf("case-%d", n+1), func(t1 *testing.T) { - // Encode - rlp, err := tt.tx.Raw() - require.NoError(t1, err) - assert.Equal(t1, tt.want, rlp) + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + tx := &TransactionOnChain{} - // Decode - tx := new(Transaction) - _, err = tx.DecodeRLP(rlp) - require.NoError(t1, err) - equalTx(t1, tt.tx, tx) - }) - } -} + err := tx.UnmarshalJSON([]byte(tt.wantJSON)) + require.NoError(t, err) + assert.Equal(t, tt.tx, tx) -func equalTx(t *testing.T, expected, got *Transaction) { - assert.Equal(t, expected.Type, got.Type) - assert.Equal(t, expected.To, got.To) - assert.Equal(t, expected.GasLimit, got.GasLimit) - assert.Equal(t, expected.GasPrice, got.GasPrice) - assert.Equal(t, expected.Input, got.Input) - assert.Equal(t, expected.Nonce, got.Nonce) - assert.Equal(t, expected.Value, got.Value) - assert.Equal(t, expected.Signature, got.Signature) - if expected.Type != LegacyTxType { - assert.Equal(t, expected.ChainID, got.ChainID) - } - assert.Equal(t, expected.MaxPriorityFeePerGas, got.MaxPriorityFeePerGas) - assert.Equal(t, expected.MaxFeePerGas, got.MaxFeePerGas) - for i, accessTuple := range expected.AccessList { - assert.Equal(t, accessTuple.Address, got.AccessList[i].Address) - assert.Equal(t, accessTuple.StorageKeys, got.AccessList[i].StorageKeys) + j, err := tx.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(j)) + }) } } diff --git a/types/tx.go b/types/tx.go new file mode 100644 index 0000000..e907172 --- /dev/null +++ b/types/tx.go @@ -0,0 +1,95 @@ +package types + +import ( + "encoding/json" + + "github.com/defiweb/go-rlp" +) + +// TransactionType is the type of transaction. +type TransactionType uint8 + +const ( + // LegacyTxType represents the legacy transaction format (Type 0). + // + // This is the original transaction format used before EIP-2718. + LegacyTxType TransactionType = iota + + // AccessListTxType represents the access list transaction format (Type 1). + // + // Introduced by EIP-2930, this transaction type includes an optional + // access list that specifies a list of addresses and storage keys the + // transaction plans to access. + AccessListTxType + + // DynamicFeeTxType represents the dynamic fee transaction format (Type 2). + // + // Introduced by EIP-1559, this transaction type supports a new fee market + // mechanism with a base fee and a priority fee (tip). + DynamicFeeTxType + + // BlobTxType represents the blob transaction format (Type 3). + // + // Introduced by EIP-4844, this transaction type adds support for + // blob-carrying transactions. + BlobTxType +) + +// Transaction is an interface that represents a generic Ethereum transaction. +type Transaction interface { + json.Marshaler + json.Unmarshaler + rlp.Encoder + rlp.Decoder + + HasTransactionData + + // Type returns the type of the transaction. + Type() TransactionType + + // Call returns the call associated with the transaction. The call is a + // copy and can be modified. It may return nil if it is impossible to + // create a call. + Call() Call + + // CalculateHash computes and returns the hash of the transaction. + CalculateHash() (Hash, error) + + // CalculateSigningHash computes and returns the hash used for signing + // the transaction. + CalculateSigningHash() (Hash, error) +} + +// TransactionDecoder is an interface for decoding transactions from JSON or +// RLP encoded data. +// +// Decoder may not set the From field of the transaction. +// To get signer of the transaction, use txsign.Recover function. +type TransactionDecoder interface { + RLPTransactionDecoder + JSONTransactionDecoder +} + +// RLPTransactionDecoder is an interface for decoding transactions from +// RLP-encoded data. +type RLPTransactionDecoder interface { + // DecodeRLP decodes the RLP encoded transaction data. + DecodeRLP(data []byte) (Transaction, error) +} + +// JSONTransactionDecoder is an interface for decoding transactions from +// JSON-encoded data. +type JSONTransactionDecoder interface { + // DecodeJSON decodes the JSON encoded transaction data. + DecodeJSON(data []byte) (Transaction, error) +} + +type jsonTransaction struct { + ChainID *Number `json:"chainId,omitempty"` + Nonce *Number `json:"nonce,omitempty"` + V *Number `json:"v,omitempty"` + R *Number `json:"r,omitempty"` + S *Number `json:"s,omitempty"` + + jsonCall +} diff --git a/types/tx_accesslist.go b/types/tx_accesslist.go new file mode 100644 index 0000000..fa213d2 --- /dev/null +++ b/types/tx_accesslist.go @@ -0,0 +1,274 @@ +package types + +import ( + "encoding/json" + "fmt" + + "github.com/defiweb/go-rlp" + + "github.com/defiweb/go-eth/crypto" +) + +// TransactionAccessList is the access list transaction type (Type 1). +// +// Introduced by EIP-2930, this transaction type includes an optional access +// list that specifies a list of addresses and storage keys the transaction +// plans to access. +type TransactionAccessList struct { + TransactionData + CallAccessList +} + +// NewTransactionAccessList creates a new access list transaction. +func NewTransactionAccessList() *TransactionAccessList { + return &TransactionAccessList{} +} + +// Type implements the Transaction interface. +func (t *TransactionAccessList) Type() TransactionType { + return AccessListTxType +} + +// Call implements the Transaction interface. +func (t *TransactionAccessList) Call() Call { + return &CallAccessList{ + CallData: *t.CallData.Copy(), + LegacyPriceData: *t.LegacyPriceData.Copy(), + AccessListData: *t.AccessListData.Copy(), + } +} + +// CalculateHash implements the Transaction interface. +func (t *TransactionAccessList) CalculateHash() (Hash, error) { + raw, err := t.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(raw)), nil +} + +// CalculateSigningHash implements the Transaction interface. +func (t *TransactionAccessList) CalculateSigningHash() (Hash, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasPrice = &rlp.BigInt{} + gasLimit = rlp.Uint(0) + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + accessList = (AccessList)(nil) + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasPrice != nil { + gasPrice = (*rlp.BigInt)(t.GasPrice) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.AccessList != nil { + accessList = t.AccessList + } + bin, err := rlp.List{ + chainID, + nonce, + gasPrice, + gasLimit, + to, + value, + input, + &accessList, + }.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(append([]byte{byte(AccessListTxType)}, bin...))), nil +} + +// EncodeRLP implements the rlp.Encoder interface. +// +//nolint:funlen +func (t TransactionAccessList) EncodeRLP() ([]byte, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasPrice = &rlp.BigInt{} + gasLimit = rlp.Uint(0) + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + accessList = (AccessList)(nil) + v = &rlp.BigInt{} + r = &rlp.BigInt{} + s = &rlp.BigInt{} + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasPrice != nil { + gasPrice = (*rlp.BigInt)(t.GasPrice) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.AccessList != nil { + accessList = t.AccessList + } + if t.Signature != nil { + v = (*rlp.BigInt)(t.Signature.V) + r = (*rlp.BigInt)(t.Signature.R) + s = (*rlp.BigInt)(t.Signature.S) + } + bin, err := rlp.List{ + chainID, + nonce, + gasPrice, + gasLimit, + to, + value, + input, + &accessList, + v, + r, + s, + }.EncodeRLP() + if err != nil { + return nil, err + } + return append([]byte{byte(AccessListTxType)}, bin...), nil +} + +// Copy creates a deep copy of the transaction. +func (t *TransactionAccessList) Copy() *TransactionAccessList { + return &TransactionAccessList{ + TransactionData: *t.TransactionData.Copy(), + CallAccessList: *t.CallAccessList.Copy(), + } +} + +// DecodeRLP implements the rlp.Decoder interface. +// +//nolint:funlen +func (t *TransactionAccessList) DecodeRLP(data []byte) (int, error) { + if len(data) == 0 { + return 0, fmt.Errorf("empty data") + } + if data[0] != byte(AccessListTxType) { + return 0, fmt.Errorf("invalid transaction type: %d", data[0]) + } + data = data[1:] + var ( + chainID = new(rlp.Uint) + nonce = new(rlp.Uint) + gasPrice = new(rlp.BigInt) + gasLimit = new(rlp.Uint) + to = new(rlp.Bytes) + value = new(rlp.BigInt) + input = new(rlp.Bytes) + accessList = new(AccessList) + v = new(rlp.BigInt) + r = new(rlp.BigInt) + s = new(rlp.BigInt) + ) + list := rlp.List{ + chainID, + nonce, + gasPrice, + gasLimit, + to, + value, + input, + accessList, + v, + r, + s, + } + if _, err := rlp.Decode(data, &list); err != nil { + return 0, err + } + *t = TransactionAccessList{} + if chainID.Get() != 0 { + t.ChainID = chainID.Ptr() + } + if nonce.Get() != 0 { + t.Nonce = nonce.Ptr() + } + if gasPrice.Get().Sign() != 0 { + t.GasPrice = gasPrice.Ptr() + } + if gasLimit.Get() != 0 { + t.GasLimit = gasLimit.Ptr() + } + if len(to.Get()) > 0 { + t.To = AddressFromBytesPtr(to.Get()) + } + if value.Ptr().Sign() != 0 { + t.Value = value.Ptr() + } + if len(input.Get()) > 0 { + t.Input = input.Get() + } + if len(*accessList) > 0 { + t.AccessList = *accessList + } + if v.Ptr().Sign() != 0 || r.Ptr().Sign() != 0 || s.Ptr().Sign() != 0 { + t.Signature = &Signature{ + V: v.Ptr(), + R: r.Ptr(), + S: s.Ptr(), + } + return len(data), nil + } + return len(data), nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (t *TransactionAccessList) MarshalJSON() ([]byte, error) { + j := &jsonTransaction{} + t.TransactionData.toJSON(j) + t.CallData.toJSON(&j.jsonCall) + t.LegacyPriceData.toJSON(&j.jsonCall) + t.AccessListData.toJSON(&j.jsonCall) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *TransactionAccessList) UnmarshalJSON(data []byte) error { + j := &jsonTransaction{} + if err := json.Unmarshal(data, &j); err != nil { + return err + } + t.TransactionData.fromJSON(j) + t.CallData.fromJSON(&j.jsonCall) + t.LegacyPriceData.fromJSON(&j.jsonCall) + t.AccessListData.fromJSON(&j.jsonCall) + return nil +} + +var _ Transaction = (*TransactionAccessList)(nil) diff --git a/types/tx_accesslist_test.go b/types/tx_accesslist_test.go new file mode 100644 index 0000000..2e754ce --- /dev/null +++ b/types/tx_accesslist_test.go @@ -0,0 +1,212 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" +) + +func TestTransactionAccessList_JSON(t *testing.T) { + tests := []struct { + name string + tx *TransactionAccessList + wantJSON string + }{ + { + name: "all fields nil", + tx: &TransactionAccessList{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + tx: &TransactionAccessList{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallAccessList: CallAccessList{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + wantJSON: `{ + "chainId": "0x1", + "nonce": "0x1", + "gasPrice": "0x3b9aca00", + "gas": "0x186a0", + "to": "0x2222222222222222222222222222222222222222", + "value": "0xde0b6b3a7640000", + "input": "0x01020304", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + "from": "0x1111111111111111111111111111111111111111" + }`, + }, + { + name: "invalid negative nonce", + tx: &TransactionAccessList{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(18446744073709551615)), // Max uint64 value to simulate negative when interpreted incorrectly + }, + }, + wantJSON: `{ + "nonce": "0xffffffffffffffff" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.tx.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Decode from JSON + tx := NewTransactionAccessList() + err = tx.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + tx.ChainID = tt.tx.ChainID + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionAccessList_RLP(t *testing.T) { + tests := []struct { + name string + tx *TransactionAccessList + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionAccessList{}, + wantHex: "0x01cb80808080808080c0808080", + }, + { + name: "all fields set", + tx: &TransactionAccessList{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallAccessList: CallAccessList{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + wantHex: "0x01f8ce0101843b9aca00830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a055555555555555555555555555555555555555555555555555555555555555556fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to RLP + rlpBytes, err := tt.tx.EncodeRLP() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, hexutil.BytesToHex(rlpBytes)) + + // Decode from RLP + tx := NewTransactionAccessList() + _, err = tx.DecodeRLP(rlpBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionAccessList_CalculateSigningHash(t *testing.T) { + tests := []struct { + name string + tx *TransactionAccessList + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionAccessList{}, + wantHex: "0xc0157440e7609b2ddee74686831421f05b238ed4c981363e64df8eb1c1ea6afc", + }, + { + name: "all fields set", + tx: &TransactionAccessList{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(1)), + }, + CallAccessList: CallAccessList{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + }, + wantHex: "0x46ba790cdf341de06f08944eecd84721e9ae3c4324098f882597d9817eeba63b", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sh, err := tt.tx.CalculateSigningHash() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, sh.String()) + }) + } +} diff --git a/types/tx_blob.go b/types/tx_blob.go new file mode 100644 index 0000000..8bc550b --- /dev/null +++ b/types/tx_blob.go @@ -0,0 +1,388 @@ +package types + +import ( + "encoding/json" + "fmt" + + "github.com/defiweb/go-rlp" + + "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/kzg4844" +) + +// TransactionBlob is the blob transaction type (Type 3). +// +// Introduced by EIP-4844, this transaction type adds support for blob-carrying +// transactions. +type TransactionBlob struct { + TransactionData + CallBlob +} + +// NewTransactionBlob creates a new blob transaction. +func NewTransactionBlob() *TransactionBlob { + return &TransactionBlob{} +} + +// Type implements the Transaction interface. +func (t *TransactionBlob) Type() TransactionType { + return BlobTxType +} + +// Call implements the Transaction interface. +func (t *TransactionBlob) Call() Call { + return t.CallBlob.Copy() +} + +// CalculateHash implements the Transaction interface. +func (t *TransactionBlob) CalculateHash() (Hash, error) { + raw, err := t.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(raw)), nil +} + +// CalculateSigningHash implements the Transaction interface. +func (t *TransactionBlob) CalculateSigningHash() (Hash, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasLimit = rlp.Uint(0) + maxPriorityFeePerGas = &rlp.BigInt{} + maxFeePerGas = &rlp.BigInt{} + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + accessList = (AccessList)(nil) + maxFeePerBlobGas = &rlp.BigInt{} + blobHashes = (rlp.TypedList[Hash])(nil) + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.MaxPriorityFeePerGas != nil { + maxPriorityFeePerGas = (*rlp.BigInt)(t.MaxPriorityFeePerGas) + } + if t.MaxFeePerGas != nil { + maxFeePerGas = (*rlp.BigInt)(t.MaxFeePerGas) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.AccessList != nil { + accessList = t.AccessList + } + if t.MaxFeePerBlobGas != nil { + maxFeePerBlobGas = (*rlp.BigInt)(t.MaxFeePerBlobGas) + } + if len(t.Blobs) > 0 { + blobHashes = make(rlp.TypedList[Hash], len(t.Blobs)) + for i := range t.Blobs { + blobHashes[i] = &t.Blobs[i].Hash + } + } + bin, err := rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + &accessList, + maxFeePerBlobGas, + &blobHashes, + }.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(append([]byte{byte(BlobTxType)}, bin...))), nil +} + +// Copy creates a deep copy of the transaction. +func (t *TransactionBlob) Copy() *TransactionBlob { + return &TransactionBlob{ + TransactionData: *t.TransactionData.Copy(), + CallBlob: *t.CallBlob.Copy(), + } +} + +// EncodeRLP implements the rlp.Encoder interface. +// +//nolint:funlen +func (t TransactionBlob) EncodeRLP() ([]byte, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasLimit = rlp.Uint(0) + maxPriorityFeePerGas = &rlp.BigInt{} + maxFeePerGas = &rlp.BigInt{} + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + accessList = (AccessList)(nil) + maxFeePerBlobGas = &rlp.BigInt{} + blobHashes = (rlp.TypedList[Hash])(nil) + blobs = (rlp.TypedList[kzgBlob])(nil) + commitments = (rlp.TypedList[kzgCommitment])(nil) + proofs = (rlp.TypedList[kzgProof])(nil) + v = &rlp.BigInt{} + r = &rlp.BigInt{} + s = &rlp.BigInt{} + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.MaxPriorityFeePerGas != nil { + maxPriorityFeePerGas = (*rlp.BigInt)(t.MaxPriorityFeePerGas) + } + if t.MaxFeePerGas != nil { + maxFeePerGas = (*rlp.BigInt)(t.MaxFeePerGas) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.AccessList != nil { + accessList = t.AccessList + } + if t.MaxFeePerBlobGas != nil { + maxFeePerBlobGas = (*rlp.BigInt)(t.MaxFeePerBlobGas) + } + if len(t.Blobs) > 0 { + blobHashes = make(rlp.TypedList[Hash], 0, len(t.Blobs)) + for i := range t.Blobs { + blob := t.Blobs[i] + + blobHashes = append(blobHashes, &blob.Hash) + if blob.Sidecar != nil { + blobs.Add((*kzgBlob)(&blob.Sidecar.Blob)) + commitments.Add((*kzgCommitment)(&blob.Sidecar.Commitment)) + proofs.Add((*kzgProof)(&blob.Sidecar.Proof)) + } + } + } + if t.Signature != nil { + v = (*rlp.BigInt)(t.Signature.V) + r = (*rlp.BigInt)(t.Signature.R) + s = (*rlp.BigInt)(t.Signature.S) + } + tx := rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + &accessList, + maxFeePerBlobGas, + &blobHashes, + v, + r, + s, + } + if len(blobHashes) > 0 && len(blobHashes) == len(blobs) { + tx = rlp.List{ + tx, + blobs, + commitments, + proofs, + } + } + bin, err := tx.EncodeRLP() + if err != nil { + return nil, err + } + return append([]byte{byte(BlobTxType)}, bin...), nil +} + +// DecodeRLP implements the rlp.Decoder interface. +// +//nolint:funlen +func (t *TransactionBlob) DecodeRLP(data []byte) (int, error) { + if len(data) == 0 { + return 0, fmt.Errorf("empty data") + } + if data[0] != byte(BlobTxType) { + return 0, fmt.Errorf("invalid transaction type: %d", data[0]) + } + data = data[1:] + var ( + chainID = new(rlp.Uint) + nonce = new(rlp.Uint) + gasLimit = new(rlp.Uint) + maxPriorityFeePerGas = new(rlp.BigInt) + maxFeePerGas = new(rlp.BigInt) + to = new(rlp.Bytes) + value = new(rlp.BigInt) + input = new(rlp.Bytes) + accessList = new(AccessList) + maxFeePerBlobGas = new(rlp.BigInt) + blobHashes = new(rlp.TypedList[Hash]) + blobs = new(rlp.TypedList[kzgBlob]) + commitments = new(rlp.TypedList[kzgCommitment]) + proofs = new(rlp.TypedList[kzgProof]) + v = new(rlp.BigInt) + r = new(rlp.BigInt) + s = new(rlp.BigInt) + ) + dec, _, err := rlp.DecodeLazy(data) + if err != nil { + return 0, err + } + if !dec.IsList() { + return 0, fmt.Errorf("unable to decode transaction") + } + var list rlp.List + switch dec.Length() { + case 4: + list = rlp.List{ + &rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + accessList, + maxFeePerBlobGas, + blobHashes, + v, + r, + s, + }, + blobs, + commitments, + proofs, + } + default: + list = rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + accessList, + maxFeePerBlobGas, + blobHashes, + v, + r, + s, + } + } + if err := dec.Decode(&list); err != nil { + return 0, err + } + *t = TransactionBlob{} + if chainID.Get() != 0 { + t.ChainID = chainID.Ptr() + } + if nonce.Get() != 0 { + t.Nonce = nonce.Ptr() + } + if maxPriorityFeePerGas.Ptr().Sign() != 0 { + t.MaxPriorityFeePerGas = maxPriorityFeePerGas.Ptr() + } + if maxFeePerGas.Ptr().Sign() != 0 { + t.MaxFeePerGas = maxFeePerGas.Ptr() + } + if gasLimit.Get() != 0 { + t.GasLimit = gasLimit.Ptr() + } + if len(to.Get()) > 0 { + t.To = AddressFromBytesPtr(to.Get()) + } + if value.Ptr().Sign() != 0 { + t.Value = value.Ptr() + } + if len(input.Get()) > 0 { + t.Input = input.Get() + } + if len(*accessList) > 0 { + t.AccessList = *accessList + } + if maxFeePerBlobGas.Ptr().Sign() != 0 { + t.MaxFeePerBlobGas = maxFeePerBlobGas.Ptr() + } + if len(*blobHashes) > 0 { + t.Blobs = make([]BlobInfo, len(*blobHashes)) + for i, hash := range *blobHashes { + blob := BlobInfo{Hash: *hash} + if i < len(*blobs) && i < len(*commitments) && i < len(*proofs) { + blob.Sidecar = &BlobSidecar{ + Blob: kzg4844.Blob(*(*blobs)[i]), + Commitment: kzg4844.Commitment(*(*commitments)[i]), + Proof: kzg4844.Proof(*(*proofs)[i]), + } + } + t.Blobs[i] = blob + } + } + if v.Ptr().Sign() != 0 || r.Ptr().Sign() != 0 || s.Ptr().Sign() != 0 { + t.Signature = &Signature{ + V: v.Ptr(), + R: r.Ptr(), + S: s.Ptr(), + } + } + return len(data), nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (t *TransactionBlob) MarshalJSON() ([]byte, error) { + j := &jsonTransaction{} + t.TransactionData.toJSON(j) + t.CallData.toJSON(&j.jsonCall) + t.AccessListData.toJSON(&j.jsonCall) + t.DynamicFeeData.toJSON(&j.jsonCall) + t.BlobData.toJSON(&j.jsonCall) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *TransactionBlob) UnmarshalJSON(data []byte) error { + j := &jsonTransaction{} + if err := json.Unmarshal(data, &j); err != nil { + return err + } + t.TransactionData.fromJSON(j) + t.CallData.fromJSON(&j.jsonCall) + t.AccessListData.fromJSON(&j.jsonCall) + t.DynamicFeeData.fromJSON(&j.jsonCall) + t.BlobData.fromJSON(&j.jsonCall) + return nil +} + +var _ Transaction = (*TransactionBlob)(nil) diff --git a/types/tx_blob_test.go b/types/tx_blob_test.go new file mode 100644 index 0000000..7495676 --- /dev/null +++ b/types/tx_blob_test.go @@ -0,0 +1,446 @@ +package types + +import ( + "math/big" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/kzg4844" + "github.com/defiweb/go-eth/hexutil" +) + +func TestTransactionBlob_JSON(t *testing.T) { + remZerosRx := regexp.MustCompile(`0{128,}`) + tests := []struct { + name string + tx *TransactionBlob + wantJSON string + }{ + { + name: "empty transaction", + tx: &TransactionBlob{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + {Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone)}, + {Hash: MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", PadNone)}, + }, + }, + }, + }, + wantJSON: `{ + "chainId": "0x1", + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxFeePerBlobGas": "0xb2d05e00", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "blobVersionedHashes": [ + "0x6666666666666666666666666666666666666666666666666666666666666666", + "0x7777777777777777777777777777777777777777777777777777777777777777" + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + }`, + }, + { + name: "blobs with shortened zero fields", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + newBlob("blob1"), + newBlob("blob2"), + }, + }, + }, + }, + wantJSON: `{ + "chainId": "0x1", + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxFeePerBlobGas": "0xb2d05e00", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "blobVersionedHashes": [ + "0x01e951827dab35ecb4ce6e29ca6779ad0ac958f06ac1d54eb6e7523f3e3febeb", + "0x01c6f423eec4f5e9ebd79e6fb5eb9bce57dd102f2f0a6fdee0bf58c4e109e27a" + ], + "blobs": [ + "0x626c6f6231", + "0x626c6f6232" + ], + "commitments": [ + "0x832726ece34fb93100194291b75b7f5fa920d5b896e5edafeed46f3636fadc485f493490bd596c76171516024bcf7a00", + "0x896a515deb6c1ac23436f5de75186316b53851be3cb4225437e58c34e376bbd6dea13d7a39b8d5dd9f6cba1c052ab0cb" + ], + "proofs": [ + "0xacbe7bde870d1e7c239063368dbf62ce3bfaef1c08006e8e724da2837294e19d991ba5677a628e64b6c6906ff1786b3c", + "0x8fd7fb4172048b9d8deccb939eeb787c45d0bb00d5f238e13084bd3fbe8050215ce488999f86fccddc36d1a257a19513" + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.tx.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(remZerosRx.ReplaceAll(jsonBytes, []byte("")))) + + // Decode from JSON + tx := NewTransactionBlob() + err = tx.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + tx.ChainID = tt.tx.ChainID + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionBlob_RLP(t *testing.T) { + tests := []struct { + name string + tx *TransactionBlob + wantHex string + wantHash bool + }{ + { + name: "empty transaction", + tx: &TransactionBlob{}, + wantHex: "0x03ce8080808080808080c080c0808080", + }, + { + name: "all fields set", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + {Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone)}, + {Hash: MustHashFromHex("0x7777777777777777777777777777777777777777777777777777777777777777", PadNone)}, + }, + }, + }, + }, + wantHex: "0x03f9011c0101843b9aca008477359400830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a0555555555555555555555555555555555555555555555555555555555555555584b2d05e00f842a06666666666666666666666666666666666666666666666666666666666666666a077777777777777777777777777777777777777777777777777777777777777776fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + { + name: "hash output", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + newBlob("blob1"), + newBlob("blob2"), + }, + }, + }, + }, + wantHex: "0x848eb4e644a60e42df3b639eb40c0f3763d13ebc2a33aa06e9b2acc22c51f59e", + wantHash: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to RLP + rlpBytes, err := tt.tx.EncodeRLP() + require.NoError(t, err) + + if tt.wantHash { + hash := crypto.Keccak256(rlpBytes) + assert.Equal(t, tt.wantHex, hexutil.BytesToHex(hash[:])) + } else { + assert.Equal(t, tt.wantHex, hexutil.BytesToHex(rlpBytes)) + } + + // Decode from RLP + tx := NewTransactionBlob() + _, err = tx.DecodeRLP(rlpBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionBlob_CalculateSigningHash(t *testing.T) { + tests := []struct { + name string + tx *TransactionBlob + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionBlob{}, + wantHex: "0x846c9b47f161837f5068b0ffb0c1a98785302f89d613338ccfa9a1c72c9f951d", + }, + { + name: "all fields set", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(1)), + }, + CallBlob: CallBlob{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + }, + }, + wantHex: "0x0604b49731147cf745c666f1a67bf1b5e9fbee127085b3d4c4958191590e8bce", + }, + { + name: "all fields set with access list", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(1)), + }, + CallBlob: CallBlob{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + AccessListData: AccessListData{ + AccessList: AccessList{ + AccessTuple{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }, + }, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + { + Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone), + }, + }, + }, + }, + }, + wantHex: "0x3faa63efab3e460606c31cd9a2e8791d87e91954137571eddb3b4b0abc69e2cd", + }, + { + name: "with blobs and access list", + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + newBlob("blob1"), + newBlob("blob2"), + }, + }, + }, + }, + wantHex: "0x09f9204d83af238e1c0044bf22b4dd52ea5c25390b27bdbd38024212e238934d", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sh, err := tt.tx.CalculateSigningHash() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, sh.String()) + }) + } +} + +func newBlob(data string) BlobInfo { + d := new(kzg4844.Blob) + copy(d[:], data) + b, err := NewBlobInfo(d) + if err != nil { + panic(err) + } + return b +} diff --git a/types/tx_decoder.go b/types/tx_decoder.go new file mode 100644 index 0000000..540946d --- /dev/null +++ b/types/tx_decoder.go @@ -0,0 +1,153 @@ +package types + +import ( + "encoding/json" + "fmt" +) + +// DefaultTransactionDecoder is used to decode transactions when no other +// decoder is specified. Default implementation is used to decode Ethereum +// transactions. +var DefaultTransactionDecoder = &TypedTransactionDecoder{ + Types: map[TransactionType]func() Transaction{ + LegacyTxType: func() Transaction { return NewTransactionLegacy() }, + AccessListTxType: func() Transaction { return NewTransactionAccessList() }, + DynamicFeeTxType: func() Transaction { return NewTransactionDynamicFee() }, + BlobTxType: func() Transaction { return NewTransactionBlob() }, + }, + IgnoreUnknownTypes: true, +} + +// TypedTransactionDecoder is am implementation of TransactionDecoder that +// could decode different types of transactions specified in the Types map. +type TypedTransactionDecoder struct { + // Types is a map of transaction types to their constructors. + Types map[TransactionType]func() Transaction + + // IgnoreUnknownTypes specifies whether to ignore unknown transaction types + // or return an error. + IgnoreUnknownTypes bool +} + +// DecodeRLP implements the RLPTransactionDecoder interface. +func (e *TypedTransactionDecoder) DecodeRLP(data []byte) (Transaction, error) { + if len(data) == 0 { + return nil, fmt.Errorf("empty transaction data") + } + typ := TransactionType(data[0]) + if typ >= 0x80 { + typ = LegacyTxType + } + tx := e.new(typ) + if e.IgnoreUnknownTypes && tx == nil { + return &TransactionUnknown{UnknownType: typ}, nil + } + if tx == nil { + return nil, fmt.Errorf("unknown transaction type: %d", typ) + } + _, err := tx.DecodeRLP(data) + if err != nil { + return nil, fmt.Errorf("failed to decode transaction: %w", err) + } + return tx, nil +} + +// DecodeJSON implements the JSONTransactionDecoder interface. +func (e *TypedTransactionDecoder) DecodeJSON(data []byte) (Transaction, error) { + typ, err := jsonTXType(data) + if err != nil { + return nil, err + } + tx := e.new(typ) + if e.IgnoreUnknownTypes && tx == nil { + return &TransactionUnknown{UnknownType: typ}, nil + } + if tx == nil { + return nil, fmt.Errorf("unknown transaction type: %d", typ) + } + if err := json.Unmarshal(data, tx); err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) + } + return tx, nil +} + +func (e *TypedTransactionDecoder) new(typ TransactionType) Transaction { + if f, ok := e.Types[typ]; ok { + return f() + } + return nil +} + +// TransactionUnknown represent a transaction of unknown type. +// +// This type is returned by TransactionDecoder when it is unable to decode a +// transaction of a specific type. +type TransactionUnknown struct { + UnknownType TransactionType +} + +func (t *TransactionUnknown) GetTransactionData() *TransactionData { return nil } + +func (t *TransactionUnknown) SetTransactionData(_ TransactionData) {} + +func (t *TransactionUnknown) Type() TransactionType { return t.UnknownType } + +func (t *TransactionUnknown) Call() Call { return nil } + +func (t *TransactionUnknown) CalculateHash() (Hash, error) { + return ZeroHash, fmt.Errorf("unable to calculate hash of unknown transaction type: %d", t.UnknownType) +} + +func (t *TransactionUnknown) CalculateSigningHash() (Hash, error) { + return ZeroHash, fmt.Errorf("unable to calculate signing hash of unknown transaction type: %d", t.UnknownType) +} + +func (t *TransactionUnknown) MarshalJSON() ([]byte, error) { + return nil, fmt.Errorf("unable to marshal unknown transaction type: %d", t.UnknownType) +} + +func (t *TransactionUnknown) UnmarshalJSON(_ []byte) error { + return fmt.Errorf("unable to unmarshal unknown transaction type: %d", t.UnknownType) +} + +func (t *TransactionUnknown) EncodeRLP() ([]byte, error) { + return nil, fmt.Errorf("unable to encode unknown transaction type: %d", t.UnknownType) +} + +func (t *TransactionUnknown) DecodeRLP(_ []byte) (int, error) { + return 0, fmt.Errorf("unable to decode unknown transaction type: %d", t.UnknownType) +} + +// jsonTXType returns the type of the transaction encoded in JSON. +// +// If type is not specified, it tries to guess the type using the same rules as +// in go-ethereum code: +// https://github.com/ethereum/go-ethereum/blob/5b3e3cd2bee284db7d7deaa5986544d356410dcb/internal/ethapi/transaction_args.go#L472 +func jsonTXType(data []byte) (TransactionType, error) { + var tx struct { + Type *Number `json:"type"` + AccessList *nilUnmarshaler `json:"accessList"` + MaxFeePerGas *nilUnmarshaler `json:"maxFeePerGas"` + BlobHashes *nilUnmarshaler `json:"blobVersionedHashes"` + } + if err := json.Unmarshal(data, &tx); err != nil { + return 0, fmt.Errorf("failed to unmarshal transaction: %w", err) + } + if tx.Type != nil { + return TransactionType((*tx.Type).Big().Uint64()), nil + } + if tx.BlobHashes != nil { + return BlobTxType, nil + } + if tx.MaxFeePerGas != nil { + return DynamicFeeTxType, nil + } + if tx.AccessList != nil { + return AccessListTxType, nil + } + return LegacyTxType, nil +} + +type nilUnmarshaler struct{} + +func (*nilUnmarshaler) UnmarshalJSON([]byte) error { return nil } diff --git a/types/tx_decoder_test.go b/types/tx_decoder_test.go new file mode 100644 index 0000000..028e240 --- /dev/null +++ b/types/tx_decoder_test.go @@ -0,0 +1,499 @@ +package types + +import ( + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" +) + +func TestTransactionDecoder_DecodeRLP(t *testing.T) { + tests := []struct { + tx Transaction + rlp string + }{ + { + tx: &TransactionLegacy{}, + rlp: "0xc9808080808080808080", + }, + { + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(38)), + Nonce: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + }, + rlp: "0xf87001843b9aca00830186a0942222222222222222222222222222222222222222880de0b6b3a764000084010203046fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + { + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(9)), + Signature: SignatureFromVRSPtr( + func() *big.Int { + v, _ := new(big.Int).SetString("37", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) + return v + }(), + ), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x3535353535353535353535353535353535353535"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(21000)), + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(20000000000), + }, + }, + }, + rlp: "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", + }, + { + tx: &TransactionAccessList{}, + rlp: "0x01cb80808080808080c0808080", + }, + { + tx: &TransactionAccessList{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallAccessList: CallAccessList{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + rlp: "0x01f8ce0101843b9aca00830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a055555555555555555555555555555555555555555555555555555555555555556fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + { + tx: &TransactionDynamicFee{}, + rlp: "0x02cc8080808080808080c0808080", + }, + { + tx: &TransactionDynamicFee{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallDynamicFee: CallDynamicFee{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + rlp: "0x02f8d30101843b9aca008477359400830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a055555555555555555555555555555555555555555555555555555555555555556fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + { + tx: &TransactionBlob{}, + rlp: "0x03ce8080808080808080c080c0808080", + }, + { + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + { + Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone), + }, + }, + }, + }, + }, + rlp: "0x03f8fa0101843b9aca008477359400830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a0555555555555555555555555555555555555555555555555555555555555555584b2d05e00e1a066666666666666666666666666666666666666666666666666666666666666666fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + tx, err := DefaultTransactionDecoder.DecodeRLP(hexutil.MustHexToBytes(tt.rlp)) + require.NoError(t, err) + assertEqualTX(t, tt.tx, tx) + }) + } +} + +func TestTransactionDeocder_DecodeJSON(t *testing.T) { + tests := []struct { + tx Transaction + json string + }{ + { + tx: &TransactionLegacy{}, + json: `{}`, + }, + { + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + }, + json: ` + { + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "gasPrice": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + } + `, + }, + { + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(9)), + Signature: SignatureFromVRSPtr( + func() *big.Int { + v, _ := new(big.Int).SetString("37", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) + return v + }(), + ), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x3535353535353535353535353535353535353535"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(21000)), + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(20000000000), + }, + }, + }, + json: ` + { + "to": "0x3535353535353535353535353535353535353535", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "nonce": "0x9", + "value": "0xde0b6b3a7640000", + "v": "0x25", + "r": "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276", + "s": "0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83" + } + `, + }, + { + tx: &TransactionAccessList{}, + json: ` + { + "type": "0x1" + } + `, + }, + { + tx: &TransactionAccessList{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallAccessList: CallAccessList{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + json: ` + { + "chainId": "0x1", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "gasPrice": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + } + `, + }, + { + tx: &TransactionDynamicFee{}, + json: ` + { + "type": "0x2" + } + `, + }, + { + tx: &TransactionDynamicFee{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallDynamicFee: CallDynamicFee{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + json: ` + { + "chainId": "0x1", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "Nonce": "0x1", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + } + `, + }, + { + tx: &TransactionBlob{}, + json: ` + { + "type": "0x3" + } + `, + }, + { + tx: &TransactionBlob{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallBlob: CallBlob{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + BlobData: BlobData{ + MaxFeePerBlobGas: big.NewInt(3000000000), + Blobs: []BlobInfo{ + { + Hash: MustHashFromHex("0x6666666666666666666666666666666666666666666666666666666666666666", PadNone), + }, + }, + }, + }, + }, + json: ` + { + "chainId": "0x1", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxFeePerBlobGas": "0xb2d05e00", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "Nonce": "0x1", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "blobVersionedHashes": [ + "0x6666666666666666666666666666666666666666666666666666666666666666" + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + } + `, + }, + { + tx: &TransactionLegacy{}, + json: ` + { + "accessList": [], + "maxFeePerGas": "0x0", + "blobVersionedHashes": [ + "0x6666666666666666666666666666666666666666666666666666666666666666" + ], + "type": "0x0" + } + `, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + tx, err := DefaultTransactionDecoder.DecodeJSON([]byte(tt.json)) + require.NoError(t, err) + assertEqualTX(t, tt.tx, tx) + }) + } +} diff --git a/types/tx_dynamicfee.go b/types/tx_dynamicfee.go new file mode 100644 index 0000000..62fe1b7 --- /dev/null +++ b/types/tx_dynamicfee.go @@ -0,0 +1,283 @@ +package types + +import ( + "encoding/json" + "fmt" + + "github.com/defiweb/go-rlp" + + "github.com/defiweb/go-eth/crypto" +) + +// TransactionDynamicFee is the dynamic fee transaction type (Type 2). +// +// Introduced by EIP-1559, this transaction type supports a new fee market +// mechanism with a base fee and a priority fee (tip). +type TransactionDynamicFee struct { + TransactionData + CallDynamicFee +} + +// NewTransactionDynamicFee creates a new dynamic fee transaction. +func NewTransactionDynamicFee() *TransactionDynamicFee { + return &TransactionDynamicFee{} +} + +// Type implements the Transaction interface. +func (t *TransactionDynamicFee) Type() TransactionType { + return DynamicFeeTxType +} + +// Call implements the Transaction interface. +func (t *TransactionDynamicFee) Call() Call { + return t.CallDynamicFee.Copy() +} + +// CalculateHash implements the Transaction interface. +func (t *TransactionDynamicFee) CalculateHash() (Hash, error) { + raw, err := t.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(raw)), nil +} + +// CalculateSigningHash implements the Transaction interface. +func (t *TransactionDynamicFee) CalculateSigningHash() (Hash, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasLimit = rlp.Uint(0) + maxPriorityFeePerGas = &rlp.BigInt{} + maxFeePerGas = &rlp.BigInt{} + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + accessList = (AccessList)(nil) + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.MaxPriorityFeePerGas != nil { + maxPriorityFeePerGas = (*rlp.BigInt)(t.MaxPriorityFeePerGas) + } + if t.MaxFeePerGas != nil { + maxFeePerGas = (*rlp.BigInt)(t.MaxFeePerGas) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.AccessList != nil { + accessList = t.AccessList + } + bin, err := rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + &accessList, + }.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(append([]byte{byte(DynamicFeeTxType)}, bin...))), nil +} + +// Copy creates a deep copy of the transaction. +func (t *TransactionDynamicFee) Copy() *TransactionDynamicFee { + return &TransactionDynamicFee{ + TransactionData: *t.TransactionData.Copy(), + CallDynamicFee: *t.CallDynamicFee.Copy(), + } +} + +// EncodeRLP implements the rlp.Encoder interface. +// +//nolint:funlen +func (t TransactionDynamicFee) EncodeRLP() ([]byte, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasLimit = rlp.Uint(0) + maxPriorityFeePerGas = &rlp.BigInt{} + maxFeePerGas = &rlp.BigInt{} + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + accessList = (AccessList)(nil) + v = &rlp.BigInt{} + r = &rlp.BigInt{} + s = &rlp.BigInt{} + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.MaxPriorityFeePerGas != nil { + maxPriorityFeePerGas = (*rlp.BigInt)(t.MaxPriorityFeePerGas) + } + if t.MaxFeePerGas != nil { + maxFeePerGas = (*rlp.BigInt)(t.MaxFeePerGas) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.AccessList != nil { + accessList = t.AccessList + } + if t.Signature != nil { + v = (*rlp.BigInt)(t.Signature.V) + r = (*rlp.BigInt)(t.Signature.R) + s = (*rlp.BigInt)(t.Signature.S) + } + bin, err := rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + &accessList, + v, + r, + s, + }.EncodeRLP() + if err != nil { + return nil, err + } + return append([]byte{byte(DynamicFeeTxType)}, bin...), nil +} + +// DecodeRLP implements the rlp.Decoder interface. +// +//nolint:funlen +func (t *TransactionDynamicFee) DecodeRLP(data []byte) (int, error) { + if len(data) == 0 { + return 0, fmt.Errorf("empty data") + } + if data[0] != byte(DynamicFeeTxType) { + return 0, fmt.Errorf("invalid transaction type: %d", data[0]) + } + data = data[1:] + var ( + chainID = new(rlp.Uint) + nonce = new(rlp.Uint) + gasLimit = new(rlp.Uint) + maxPriorityFeePerGas = new(rlp.BigInt) + maxFeePerGas = new(rlp.BigInt) + to = new(rlp.Bytes) + value = new(rlp.BigInt) + input = new(rlp.Bytes) + accessList = new(AccessList) + v = new(rlp.BigInt) + r = new(rlp.BigInt) + s = new(rlp.BigInt) + ) + list := rlp.List{ + chainID, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + input, + accessList, + v, + r, + s, + } + if _, err := rlp.Decode(data, &list); err != nil { + return 0, err + } + *t = TransactionDynamicFee{} + if chainID.Get() != 0 { + t.ChainID = chainID.Ptr() + } + if nonce.Get() != 0 { + t.Nonce = nonce.Ptr() + } + if maxPriorityFeePerGas.Ptr().Sign() != 0 { + t.MaxPriorityFeePerGas = maxPriorityFeePerGas.Ptr() + } + if maxFeePerGas.Ptr().Sign() != 0 { + t.MaxFeePerGas = maxFeePerGas.Ptr() + } + if gasLimit.Get() != 0 { + t.GasLimit = gasLimit.Ptr() + } + if len(to.Get()) > 0 { + t.To = AddressFromBytesPtr(to.Get()) + } + if value.Ptr().Sign() != 0 { + t.Value = value.Ptr() + } + if len(input.Get()) > 0 { + t.Input = input.Get() + } + if len(*accessList) > 0 { + t.AccessList = *accessList + } + if v.Ptr().Sign() != 0 || r.Ptr().Sign() != 0 || s.Ptr().Sign() != 0 { + t.Signature = &Signature{ + V: v.Ptr(), + R: r.Ptr(), + S: s.Ptr(), + } + } + return len(data), nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (t *TransactionDynamicFee) MarshalJSON() ([]byte, error) { + j := &jsonTransaction{} + t.TransactionData.toJSON(j) + t.CallData.toJSON(&j.jsonCall) + t.AccessListData.toJSON(&j.jsonCall) + t.DynamicFeeData.toJSON(&j.jsonCall) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *TransactionDynamicFee) UnmarshalJSON(data []byte) error { + j := &jsonTransaction{} + if err := json.Unmarshal(data, j); err != nil { + return err + } + t.TransactionData.fromJSON(j) + t.CallData.fromJSON(&j.jsonCall) + t.AccessListData.fromJSON(&j.jsonCall) + t.DynamicFeeData.fromJSON(&j.jsonCall) + return nil +} + +var _ Transaction = (*TransactionDynamicFee)(nil) diff --git a/types/tx_dynamicfee_test.go b/types/tx_dynamicfee_test.go new file mode 100644 index 0000000..c4029a3 --- /dev/null +++ b/types/tx_dynamicfee_test.go @@ -0,0 +1,239 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" +) + +func TestTransactionDynamicFee_JSON(t *testing.T) { + tests := []struct { + name string + tx *TransactionDynamicFee + wantJSON string + }{ + { + name: "empty transaction", + tx: &TransactionDynamicFee{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + tx: &TransactionDynamicFee{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallDynamicFee: CallDynamicFee{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + wantJSON: `{ + "chainId": "0x1", + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxPriorityFeePerGas": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "accessList": [ + { + "address": "0x3333333333333333333333333333333333333333", + "storageKeys": [ + "0x4444444444444444444444444444444444444444444444444444444444444444", + "0x5555555555555555555555555555555555555555555555555555555555555555" + ] + } + ], + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.tx.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Decode from JSON + tx := NewTransactionDynamicFee() + err = tx.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + tx.ChainID = tt.tx.ChainID + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionDynamicFee_RLP(t *testing.T) { + tests := []struct { + name string + tx *TransactionDynamicFee + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionDynamicFee{}, + wantHex: "0x02cc8080808080808080c0808080", + }, + { + name: "all fields set", + tx: &TransactionDynamicFee{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + ChainID: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallDynamicFee: CallDynamicFee{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + AccessListData: AccessListData{ + AccessList: []AccessTuple{{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }}, + }, + }, + }, + wantHex: "0x02f8d30101843b9aca008477359400830186a0942222222222222222222222222222222222222222880de0b6b3a76400008401020304f85bf859943333333333333333333333333333333333333333f842a04444444444444444444444444444444444444444444444444444444444444444a055555555555555555555555555555555555555555555555555555555555555556fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to RLP + rlpBytes, err := tt.tx.EncodeRLP() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, hexutil.BytesToHex(rlpBytes)) + + // Decode from RLP + tx := NewTransactionDynamicFee() + _, err = tx.DecodeRLP(rlpBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + tx.ChainID = tt.tx.ChainID + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionDynamicFee_CalculateSigningHash(t *testing.T) { + tests := []struct { + name string + tx *TransactionDynamicFee + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionDynamicFee{}, + wantHex: "0x292edeba1be7c90f4dbaed50c44b7f6378633f933202ffe4f547e5a5c2ca3304", + }, + { + name: "all fields set", + tx: &TransactionDynamicFee{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(1)), + }, + CallDynamicFee: CallDynamicFee{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + }, + }, + wantHex: "0xc3266152306909bfe339f90fad4f73f958066860300b5a22b98ee6a1d629706c", + }, + { + name: "all fields set with access list", + tx: &TransactionDynamicFee{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(1)), + }, + CallDynamicFee: CallDynamicFee{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + AccessListData: AccessListData{ + AccessList: AccessList{ + AccessTuple{ + Address: MustAddressFromHex("0x3333333333333333333333333333333333333333"), + StorageKeys: []Hash{ + MustHashFromHex("0x4444444444444444444444444444444444444444444444444444444444444444", PadNone), + MustHashFromHex("0x5555555555555555555555555555555555555555555555555555555555555555", PadNone), + }, + }, + }, + }, + DynamicFeeData: DynamicFeeData{ + MaxPriorityFeePerGas: big.NewInt(1000000000), + MaxFeePerGas: big.NewInt(2000000000), + }, + }, + }, + wantHex: "0xa66ab756479bfd56f29658a8a199319094e84711e8a2de073ec136ef5179c4c9", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sh, err := tt.tx.CalculateSigningHash() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, sh.String()) + }) + } +} diff --git a/types/tx_legacy.go b/types/tx_legacy.go new file mode 100644 index 0000000..ec26017 --- /dev/null +++ b/types/tx_legacy.go @@ -0,0 +1,247 @@ +package types + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/defiweb/go-rlp" + + "github.com/defiweb/go-eth/crypto" +) + +// TransactionLegacy is the legacy transaction type (Type 0). +// +// This is the original transaction format used before EIP-2718. +type TransactionLegacy struct { + TransactionData + CallLegacy +} + +// NewTransactionLegacy creates a new legacy transaction. +func NewTransactionLegacy() *TransactionLegacy { + return &TransactionLegacy{} +} + +// Type implements the Transaction interface. +func (t *TransactionLegacy) Type() TransactionType { + return LegacyTxType +} + +// Call implements the Transaction interface. +func (t *TransactionLegacy) Call() Call { + return t.CallLegacy.Copy() +} + +// CalculateHash implements the Transaction interface. +func (t *TransactionLegacy) CalculateHash() (Hash, error) { + raw, err := t.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(raw)), nil +} + +// CalculateSigningHash implements the Transaction interface. +func (t *TransactionLegacy) CalculateSigningHash() (Hash, error) { + var ( + chainID = rlp.Uint(0) + nonce = rlp.Uint(0) + gasPrice = &rlp.BigInt{} + gasLimit = rlp.Uint(0) + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + ) + if t.ChainID != nil { + chainID = rlp.Uint(*t.ChainID) + } + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasPrice != nil { + gasPrice = (*rlp.BigInt)(t.GasPrice) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + list := rlp.List{ + nonce, + gasPrice, + gasLimit, + to, + value, + input, + } + if t.ChainID != nil && *t.ChainID != 0 { + list.Add( + chainID, + rlp.Uint(0), + rlp.Uint(0), + ) + } + bin, err := list.EncodeRLP() + if err != nil { + return ZeroHash, err + } + return Hash(crypto.Keccak256(bin)), nil +} + +// Copy creates a deep copy of the transaction. +func (t *TransactionLegacy) Copy() *TransactionLegacy { + return &TransactionLegacy{ + TransactionData: *t.TransactionData.Copy(), + CallLegacy: *t.CallLegacy.Copy(), + } +} + +// EncodeRLP implements the rlp.Encoder interface. +// +//nolint:funlen +func (t TransactionLegacy) EncodeRLP() ([]byte, error) { + var ( + nonce = rlp.Uint(0) + gasPrice = &rlp.BigInt{} + gasLimit = rlp.Uint(0) + to = (rlp.Bytes)(nil) + value = &rlp.BigInt{} + input = (rlp.Bytes)(nil) + v = &rlp.BigInt{} + r = &rlp.BigInt{} + s = &rlp.BigInt{} + ) + if t.Nonce != nil { + nonce = rlp.Uint(*t.Nonce) + } + if t.GasPrice != nil { + gasPrice = (*rlp.BigInt)(t.GasPrice) + } + if t.GasLimit != nil { + gasLimit = rlp.Uint(*t.GasLimit) + } + if t.To != nil { + to = t.To[:] + } + if t.Value != nil { + value = (*rlp.BigInt)(t.Value) + } + if t.Input != nil { + input = t.Input + } + if t.Signature != nil { + v = (*rlp.BigInt)(t.Signature.V) + r = (*rlp.BigInt)(t.Signature.R) + s = (*rlp.BigInt)(t.Signature.S) + } + return rlp.List{ + nonce, + gasPrice, + gasLimit, + to, + value, + input, + v, + r, + s, + }.EncodeRLP() +} + +// DecodeRLP implements the rlp.Decoder interface. +// +//nolint:funlen +func (t *TransactionLegacy) DecodeRLP(data []byte) (int, error) { + if len(data) == 0 { + return 0, fmt.Errorf("empty data") + } + var ( + nonce = new(rlp.Uint) + gasPrice = new(rlp.BigInt) + gasLimit = new(rlp.Uint) + to = new(rlp.Bytes) + value = new(rlp.BigInt) + input = new(rlp.Bytes) + v = new(rlp.BigInt) + r = new(rlp.BigInt) + s = new(rlp.BigInt) + ) + list := rlp.List{ + nonce, + gasPrice, + gasLimit, + to, + value, + input, + v, + r, + s, + } + if _, err := rlp.Decode(data, &list); err != nil { + return 0, err + } + *t = TransactionLegacy{} + if nonce.Get() != 0 { + t.Nonce = nonce.Ptr() + } + if gasPrice.Get().Sign() != 0 { + t.GasPrice = gasPrice.Ptr() + } + if gasLimit.Get() != 0 { + t.GasLimit = gasLimit.Ptr() + } + if len(to.Get()) > 0 { + t.To = AddressFromBytesPtr(*to) + } + if value.Get().Sign() != 0 { + t.Value = value.Ptr() + } + if len(input.Get()) > 0 { + t.Input = input.Get() + } + if v.Get().Sign() != 0 || r.Get().Sign() != 0 || s.Get().Sign() != 0 { + t.Signature = &Signature{ + V: (*big.Int)(v), + R: (*big.Int)(r), + S: (*big.Int)(s), + } + // Derive chain ID from the V value. + if v.Get().Cmp(big.NewInt(35)) >= 0 { + x := new(big.Int).Sub((*big.Int)(v), big.NewInt(35)) + x = x.Div(x, big.NewInt(2)) + chainID := x.Uint64() + t.ChainID = &chainID + } + } + return len(data), nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (t *TransactionLegacy) MarshalJSON() ([]byte, error) { + j := &jsonTransaction{} + t.TransactionData.toJSON(j) + t.CallData.toJSON(&j.jsonCall) + t.LegacyPriceData.toJSON(&j.jsonCall) + return json.Marshal(j) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *TransactionLegacy) UnmarshalJSON(data []byte) error { + j := &jsonTransaction{} + if err := json.Unmarshal(data, &j); err != nil { + return err + } + t.TransactionData.fromJSON(j) + t.CallData.fromJSON(&j.jsonCall) + t.LegacyPriceData.fromJSON(&j.jsonCall) + return nil +} + +var _ Transaction = (*TransactionLegacy)(nil) diff --git a/types/tx_legacy_test.go b/types/tx_legacy_test.go new file mode 100644 index 0000000..a812533 --- /dev/null +++ b/types/tx_legacy_test.go @@ -0,0 +1,271 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defiweb/go-eth/hexutil" +) + +func TestTransactionLegacy_JSON(t *testing.T) { + tests := []struct { + name string + tx *TransactionLegacy + wantJSON string + }{ + { + name: "empty transaction", + tx: &TransactionLegacy{}, + wantJSON: `{}`, + }, + { + name: "all fields set", + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + }, + wantJSON: `{ + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "gas": "0x186a0", + "gasPrice": "0x3b9aca00", + "input": "0x01020304", + "nonce": "0x1", + "value": "0xde0b6b3a7640000", + "v": "0x6f", + "r": "0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490", + "s": "0x8051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84" + }`, + }, + { + name: "example from EIP-155", + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(9)), + Signature: SignatureFromVRSPtr( + func() *big.Int { + v, _ := new(big.Int).SetString("37", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) + return v + }(), + ), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x3535353535353535353535353535353535353535"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(21000)), + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(20000000000), + }, + }, + }, + wantJSON: `{ + "chainId": "0x1", + "to": "0x3535353535353535353535353535353535353535", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "nonce": "0x9", + "value": "0xde0b6b3a7640000", + "v": "0x25", + "r": "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276", + "s": "0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83" + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to JSON + jsonBytes, err := tt.tx.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(jsonBytes)) + + // Decode from JSON + tx := NewTransactionLegacy() + err = tx.UnmarshalJSON(jsonBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + tx.ChainID = tt.tx.ChainID + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionLegacy_RLP(t *testing.T) { + tests := []struct { + name string + tx *TransactionLegacy + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionLegacy{}, + wantHex: "0xc9808080808080808080", + }, + { + name: "all fields set", + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + Nonce: ptr(uint64(1)), + Signature: MustSignatureFromHexPtr("0xa3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad914908051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd846f"), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + }, + wantHex: "0xf87001843b9aca00830186a0942222222222222222222222222222222222222222880de0b6b3a764000084010203046fa0a3a7b12762dbc5df6cfbedbecdf8a821929c6112d2634abbb0d99dc63ad91490a08051b2c8c7d159db49ad19bd01026156eedab2f3d8c1dfdd07d21c07a4bbdd84", + }, + { + name: "example from EIP-155", + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(9)), + Signature: SignatureFromVRSPtr( + func() *big.Int { + v, _ := new(big.Int).SetString("37", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) + return v + }(), + func() *big.Int { + v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) + return v + }(), + ), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x3535353535353535353535353535353535353535"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(21000)), + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(20000000000), + }, + }, + }, + wantHex: "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Encode to RLP + rlpBytes, err := tt.tx.EncodeRLP() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, hexutil.BytesToHex(rlpBytes)) + + // Decode from RLP + tx := NewTransactionLegacy() + _, err = tx.DecodeRLP(rlpBytes) + require.NoError(t, err) + + // Compare the original and decoded transactions + tx.From = tt.tx.From + tx.ChainID = tt.tx.ChainID + assertEqualTX(t, tx, tt.tx) + }) + } +} + +func TestTransactionLegacy_CalculateSigningHash(t *testing.T) { + tests := []struct { + name string + tx *TransactionLegacy + wantHex string + }{ + { + name: "empty transaction", + tx: &TransactionLegacy{}, + wantHex: "0x5460be86ce1e4ca0564b5761c6e7070d9f054b671f5404268335000806423d75", + }, + { + name: "all fields set", + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(1)), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + From: MustAddressFromHexPtr("0x1111111111111111111111111111111111111111"), + To: MustAddressFromHexPtr("0x2222222222222222222222222222222222222222"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(100000)), + Input: []byte{1, 2, 3, 4}, + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(1000000000), + }, + }, + }, + wantHex: "0x1efbe489013ac8c0dad2202f68ac12657471df8d80f70e0683ec07b0564a32ca", + }, + { + name: "example from EIP-155", + tx: &TransactionLegacy{ + TransactionData: TransactionData{ + ChainID: ptr(uint64(1)), + Nonce: ptr(uint64(9)), + }, + CallLegacy: CallLegacy{ + CallData: CallData{ + To: MustAddressFromHexPtr("0x3535353535353535353535353535353535353535"), + Value: big.NewInt(1000000000000000000), + GasLimit: ptr(uint64(21000)), + }, + LegacyPriceData: LegacyPriceData{ + GasPrice: big.NewInt(20000000000), + }, + }, + }, + wantHex: "0xdaf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sh, err := tt.tx.CalculateSigningHash() + require.NoError(t, err) + assert.Equal(t, tt.wantHex, sh.String()) + }) + } +} diff --git a/types/types.go b/types/types.go index 4bcaf41..79a8b28 100644 --- a/types/types.go +++ b/types/types.go @@ -9,11 +9,19 @@ import ( "github.com/defiweb/go-rlp" + "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/kzg4844" "github.com/defiweb/go-eth/hexutil" ) -// HashFunc returns the hash for the given input. -type HashFunc func(data ...[]byte) Hash +var ( + // ForceAddressChecksum is a global flag that forces the use of checksummed + // addresses in text representations. + // + // Note: This is a global flag, so it affects all packages that use the + // Address type. + ForceAddressChecksum = false +) // Pad is a padding type. type Pad uint8 @@ -40,6 +48,9 @@ var ZeroAddress = Address{} // AddressFromHex parses an address in hex format and returns an Address type. func AddressFromHex(h string) (a Address, err error) { err = a.UnmarshalText([]byte(h)) + if err != nil { + return ZeroAddress, err + } return a, err } @@ -107,6 +118,15 @@ func MustAddressFromBytesPtr(b []byte) *Address { return &a } +// VerifyAddressChecksum verifies if the given cheksummed address is valid. +func VerifyAddressChecksum(h string) bool { + a, err := AddressFromHex(h) + if err != nil { + return false + } + return a.Checksum() == h +} + // Bytes returns the byte representation of the address. func (t Address) Bytes() []byte { return t[:] @@ -114,17 +134,17 @@ func (t Address) Bytes() []byte { // String returns the hex representation of the address. func (t Address) String() string { + if ForceAddressChecksum { + return t.Checksum() + } return hexutil.BytesToHex(t[:]) } // Checksum returns the address with the checksum calculated according to // EIP-55. -// -// HashFunc is the hash function used to calculate the checksum, most likely -// crypto.Keccak256. -func (t Address) Checksum(h HashFunc) string { +func (t Address) Checksum() string { hex := []byte(hexutil.BytesToHex(t[:])[2:]) - hash := h(hex) + hash := crypto.Keccak256(hex) for i, c := range hex { if c >= '0' && c <= '9' { continue @@ -141,43 +161,55 @@ func (t Address) IsZero() bool { return t == ZeroAddress } +// MarshalJSON implements the json.Marshaler interface. func (t Address) MarshalJSON() ([]byte, error) { + if ForceAddressChecksum { + return naiveQuote([]byte(t.Checksum())), nil + } return bytesMarshalJSON(t[:]), nil } +// UnmarshalJSON implements the json.Unmarshaler interface. func (t *Address) UnmarshalJSON(input []byte) error { return fixedBytesUnmarshalJSON(input, t[:]) } +// MarshalText implements the encoding.TextMarshaler interface. func (t Address) MarshalText() ([]byte, error) { + if ForceAddressChecksum { + return []byte(t.Checksum()), nil + } return bytesMarshalText(t[:]), nil } +// UnmarshalText implements the encoding.TextUnmarshaler interface. func (t *Address) UnmarshalText(input []byte) error { return fixedBytesUnmarshalText(input, t[:]) } +// EncodeRLP implements the rlp.Encoder interface. func (t Address) EncodeRLP() ([]byte, error) { - return rlp.Encode(rlp.NewBytes(t[:])) + return rlp.Encode(rlp.Bytes(t[:])) } +// DecodeRLP implements the rlp.Decoder interface. func (t *Address) DecodeRLP(data []byte) (int, error) { - r, n, err := rlp.Decode(data) + r, n, err := rlp.DecodeLazy(data) if err != nil { return 0, err } - a, err := r.GetBytes() + b, err := r.Bytes() if err != nil { return 0, err } - if len(a) == 0 { + if len(b) == 0 { *t = ZeroAddress return n, nil } - if len(a) != AddressLength { - return 0, fmt.Errorf("invalid address length %d", len(a)) + if len(b) != AddressLength { + return 0, fmt.Errorf("invalid address length %d", len(b)) } - copy(t[:], a) + copy(t[:], b) return n, nil } @@ -193,6 +225,11 @@ type Hash [HashLength]byte // ZeroHash is a hash with all zeros. var ZeroHash = Hash{} +// HashKeccak256 calculates the Keccak256 hash of the given data. +func HashKeccak256(data ...[]byte) Hash { + return Hash(crypto.Keccak256(data...)) +} + // HashFromHex parses a hash in hex format and returns a Hash type. // If hash is longer than 32 bytes, it returns an error. func HashFromHex(h string, pad Pad) (Hash, error) { @@ -349,35 +386,45 @@ func (t Hash) IsZero() bool { return t == ZeroHash } +// MarshalJSON implements the json.Marshaler interface. func (t Hash) MarshalJSON() ([]byte, error) { return bytesMarshalJSON(t[:]), nil } +// UnmarshalJSON implements the json.Unmarshaler interface. func (t *Hash) UnmarshalJSON(input []byte) error { return fixedBytesUnmarshalJSON(input, t[:]) } +// MarshalText implements the encoding.TextMarshaler interface. func (t Hash) MarshalText() ([]byte, error) { return bytesMarshalText(t[:]), nil } +// UnmarshalText implements the encoding.TextUnmarshaler interface. func (t *Hash) UnmarshalText(input []byte) error { return fixedBytesUnmarshalText(input, t[:]) } +// EncodeRLP implements the rlp.Encoder interface. func (t Hash) EncodeRLP() ([]byte, error) { - return rlp.Encode(rlp.NewBytes(t[:])) + return rlp.Encode(rlp.Bytes(t[:])) } +// DecodeRLP implements the rlp.Decoder interface. func (t *Hash) DecodeRLP(data []byte) (int, error) { - r, n, err := rlp.Decode(data) + r, n, err := rlp.DecodeLazy(data) if err != nil { return 0, err } - b, err := r.GetBytes() + b, err := r.Bytes() if err != nil { return 0, err } + if len(b) == 0 { + *t = ZeroHash + return n, nil + } if len(b) != HashLength { return 0, fmt.Errorf("invalid hash length %d", len(t)) } @@ -507,6 +554,12 @@ func (t *BlockNumber) IsTag() bool { } // Big returns the big.Int representation of the block number. +// It returns a negative number if the block tag is used: +// - earliest: -1 +// - latest: -2 +// - pending: -3 +// - safe: -4 +// - finalized: -5 func (t *BlockNumber) Big() *big.Int { return new(big.Int).Set(&t.x) } @@ -529,6 +582,7 @@ func (t *BlockNumber) String() string { } } +// MarshalJSON implements the json.Marshaler interface. func (t BlockNumber) MarshalJSON() ([]byte, error) { b, err := t.MarshalText() if err != nil { @@ -537,10 +591,16 @@ func (t BlockNumber) MarshalJSON() ([]byte, error) { return naiveQuote(b), nil } +// UnmarshalJSON implements the json.Unmarshaler interface. func (t *BlockNumber) UnmarshalJSON(input []byte) error { - return t.UnmarshalText(naiveUnquote(input)) + input, ok := naiveUnquote(input) + if !ok { + return fmt.Errorf("invalid JSON string: %s", input) + } + return t.UnmarshalText(input) } +// MarshalText implements the encoding.TextMarshaler interface. func (t BlockNumber) MarshalText() ([]byte, error) { switch { case t.IsEarliest(): @@ -558,6 +618,7 @@ func (t BlockNumber) MarshalText() ([]byte, error) { } } +// UnmarshalText implements the encoding.TextUnmarshaler interface. func (t *BlockNumber) UnmarshalText(input []byte) error { switch strings.ToLower(strings.TrimSpace(string(input))) { case "earliest": @@ -774,6 +835,7 @@ func (s Signature) Equal(c Signature) bool { return sv.Cmp(cv) == 0 && sr.Cmp(cr) == 0 && ss.Cmp(cs) == 0 } +// Copy returns a deep copy of the signature. func (s Signature) Copy() *Signature { cpy := &Signature{} if s.V != nil { @@ -788,10 +850,12 @@ func (s Signature) Copy() *Signature { return cpy } +// MarshalJSON implements the json.Marshaler interface. func (s Signature) MarshalJSON() ([]byte, error) { return bytesMarshalJSON(s.Bytes()), nil } +// UnmarshalJSON implements the json.Unmarshaler interface. func (s *Signature) UnmarshalJSON(input []byte) error { var b []byte if err := bytesUnmarshalJSON(input, &b); err != nil { @@ -805,10 +869,12 @@ func (s *Signature) UnmarshalJSON(input []byte) error { return nil } +// MarshalText implements the encoding.TextMarshaler interface. func (s Signature) MarshalText() ([]byte, error) { return bytesMarshalText(s.Bytes()), nil } +// UnmarshalText implements the encoding.TextUnmarshaler interface. func (s *Signature) UnmarshalText(input []byte) error { var b []byte if err := bytesUnmarshalText(input, &b); err != nil { @@ -910,7 +976,7 @@ func (t *Number) Big() *big.Int { return new(big.Int).Set(&t.x) } -// Bytes returns the byte representation of the number. +// Bytes returns the absolute value of a number as a big-endian byte slice. func (t *Number) Bytes() []byte { return t.x.Bytes() } @@ -920,18 +986,22 @@ func (t *Number) String() string { return hexutil.BigIntToHex(&t.x) } +// MarshalJSON implements the json.Marshaler interface. func (t Number) MarshalJSON() ([]byte, error) { return numberMarshalJSON(t.Big()), nil } +// UnmarshalJSON implements the json.Unmarshaler interface. func (t *Number) UnmarshalJSON(input []byte) error { return numberUnmarshalJSON(input, &t.x) } +// MarshalText implements the encoding.TextMarshaler interface. func (t Number) MarshalText() ([]byte, error) { return numberMarshalText(t.Big()), nil } +// UnmarshalText implements the encoding.TextUnmarshaler interface. func (t *Number) UnmarshalText(input []byte) error { return numberUnmarshalText(input, &t.x) } @@ -1022,154 +1092,159 @@ func (b *Bytes) String() string { return hexutil.BytesToHex(*b) } +// MarshalJSON implements the json.Marshaler interface. func (b Bytes) MarshalJSON() ([]byte, error) { return bytesMarshalJSON(b), nil } +// UnmarshalJSON implements the json.Unmarshaler interface. func (b *Bytes) UnmarshalJSON(input []byte) error { return bytesUnmarshalJSON(input, (*[]byte)(b)) } +// MarshalText implements the encoding.TextMarshaler interface. func (b Bytes) MarshalText() ([]byte, error) { return bytesMarshalText(b), nil } +// UnmarshalText implements the encoding.TextUnmarshaler interface. func (b *Bytes) UnmarshalText(input []byte) error { return bytesUnmarshalText(input, (*[]byte)(b)) } -// -// SyncStatus type: -// - -// SyncStatus represents the sync status of a node. -type SyncStatus struct { - StartingBlock BlockNumber `json:"startingBlock"` - CurrentBlock BlockNumber `json:"currentBlock"` - HighestBlock BlockNumber `json:"highestBlock"` -} - // // Internal types: // -const bloomLength = 256 +const ( + bloomLength = 256 + nonceLength = 8 +) -type hexBloom [bloomLength]byte +// oneOrList is a type that can marshal and unmarshal a single element or a list +// of elements. +type oneOrList[T any] []T -func bloomFromBytes(x []byte) hexBloom { - var b [bloomLength]byte - if len(x) > len(b) { - return b +func (l oneOrList[T]) MarshalJSON() ([]byte, error) { + if len(l) == 1 { + return json.Marshal(l[0]) } - copy(b[bloomLength-len(x):], x) - return b -} - -func (t *hexBloom) Bytes() []byte { - return t[:] + return json.Marshal([]T(l)) } -func (t *hexBloom) String() string { - if t == nil { - return "" +func (l *oneOrList[T]) UnmarshalJSON(input []byte) error { + if len(input) >= 1 && input[0] == '[' || input[0] == '{' { + return json.Unmarshal(input, l) } - return hexutil.BytesToHex(t[:]) + var i T + if err := json.Unmarshal(input, &i); err != nil { + return err + } + *l = oneOrList[T]{i} + return nil } -func (t hexBloom) MarshalJSON() ([]byte, error) { +// kzgBlob is a fixed-length byte array used for KZG blob. +type kzgBlob [kzg4844.BlobLength]byte + +func (t kzgBlob) MarshalJSON() ([]byte, error) { return bytesMarshalJSON(t[:]), nil } -func (t *hexBloom) UnmarshalJSON(input []byte) error { +func (t *kzgBlob) UnmarshalJSON(input []byte) error { return fixedBytesUnmarshalJSON(input, t[:]) } -func (t hexBloom) MarshalText() ([]byte, error) { - return bytesMarshalText(t[:]), nil +func (t kzgBlob) EncodeRLP() ([]byte, error) { + return rlp.Encode(rlp.Bytes(t[:])) } -func (t *hexBloom) UnmarshalText(input []byte) error { - return fixedBytesUnmarshalText(input, t[:]) +func (t *kzgBlob) DecodeRLP(data []byte) (int, error) { + return fixedBytesDecodeRLP(data, t[:]) } -const nonceLength = 8 +// kzgCommitment is a fixed-length byte array used for KZG commitment. +type kzgCommitment [kzg4844.CommitmentLength]byte -type hexNonce [nonceLength]byte - -func nonceFromBigInt(x *big.Int) hexNonce { - if x == nil { - return hexNonce{} - } - return nonceFromBytes(x.Bytes()) +func (t kzgCommitment) MarshalJSON() ([]byte, error) { + return bytesMarshalJSON(t[:]), nil } -func nonceFromBytes(x []byte) hexNonce { - var n hexNonce - if len(x) > len(n) { - return n - } - copy(n[nonceLength-len(x):], x) - return n +func (t *kzgCommitment) UnmarshalJSON(input []byte) error { + return fixedBytesUnmarshalJSON(input, t[:]) } -func (t *hexNonce) Big() *big.Int { - return new(big.Int).SetBytes(t[:]) +func (t kzgCommitment) EncodeRLP() ([]byte, error) { + return rlp.Encode(rlp.Bytes(t[:])) } -func (t *hexNonce) String() string { - if t == nil { - return "" - } - return hexutil.BytesToHex(t[:]) +func (t *kzgCommitment) DecodeRLP(data []byte) (int, error) { + return fixedBytesDecodeRLP(data, t[:]) } -func (t hexNonce) MarshalJSON() ([]byte, error) { +// kzgProof is a fixed-length byte array used for KZG proof. +type kzgProof [kzg4844.ProofLength]byte + +func (t kzgProof) MarshalJSON() ([]byte, error) { return bytesMarshalJSON(t[:]), nil } -func (t *hexNonce) UnmarshalJSON(input []byte) error { +func (t *kzgProof) UnmarshalJSON(input []byte) error { return fixedBytesUnmarshalJSON(input, t[:]) } -func (t hexNonce) MarshalText() ([]byte, error) { - return bytesMarshalText(t[:]), nil +func (t kzgProof) EncodeRLP() ([]byte, error) { + return rlp.Encode(rlp.Bytes(t[:])) } -func (t *hexNonce) UnmarshalText(input []byte) error { - return fixedBytesUnmarshalText(input, t[:]) +func (t *kzgProof) DecodeRLP(data []byte) (int, error) { + return fixedBytesDecodeRLP(data, t[:]) } -type hashList []Hash +// bloom is a fixed-length byte array used for bloom filter. +type bloom [bloomLength]byte -func (b hashList) MarshalJSON() ([]byte, error) { - if len(b) == 1 { - return json.Marshal(b[0]) +func bloomFromBytes(x []byte) bloom { + var b [bloomLength]byte + if len(x) > len(b) { + return b } - return json.Marshal([]Hash(b)) + copy(b[bloomLength-len(x):], x) + return b } -func (b *hashList) UnmarshalJSON(input []byte) error { - if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' { - *b = hashList{{}} - return json.Unmarshal(input, &((*b)[0])) - } - return json.Unmarshal(input, (*[]Hash)(b)) +func (t *bloom) Bytes() []byte { + return t[:] } -type addressList []Address +func (t bloom) MarshalJSON() ([]byte, error) { + return bytesMarshalJSON(t[:]), nil +} -func (t addressList) MarshalJSON() ([]byte, error) { - if len(t) == 1 { - return json.Marshal(t[0]) - } - return json.Marshal([]Address(t)) +func (t *bloom) UnmarshalJSON(input []byte) error { + return fixedBytesUnmarshalJSON(input, t[:]) } -func (t *addressList) UnmarshalJSON(input []byte) error { - if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' { - *t = addressList{{}} - return json.Unmarshal(input, &((*t)[0])) +func nonceFromBigInt(x *big.Int) (n nonce) { + if x == nil { + return nonce{} } - return json.Unmarshal(input, (*[]Address)(t)) + b := x.Bytes() + copy(n[nonceLength-len(b):], b) + return n +} + +// nonce is a fixed-length byte array used for nonce. +type nonce [nonceLength]byte + +func (t *nonce) Big() *big.Int { + return new(big.Int).SetBytes(t[:]) +} + +func (t nonce) MarshalJSON() ([]byte, error) { + return bytesMarshalJSON(t[:]), nil +} + +func (t *nonce) UnmarshalJSON(input []byte) error { + return fixedBytesUnmarshalJSON(input, t[:]) } diff --git a/types/types_test.go b/types/types_test.go index b6e6c4a..10cb741 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -7,181 +7,181 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/crypto/sha3" + + "github.com/defiweb/go-eth/hexutil" ) -func Test_AddressType_Unmarshal(t *testing.T) { +func Test_AddressFromHex(t *testing.T) { tests := []struct { arg string want Address wantErr bool }{ { - arg: `"0x00112233445566778899aabbccddeeff00112233"`, + arg: "0x00112233445566778899aabbccddeeff00112233", want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), }, { - arg: `"00112233445566778899aabbccddeeff00112233"`, + arg: "00112233445566778899aabbccddeeff00112233", want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), }, { - arg: `"00112233445566778899aabbccddeeff0011223344"`, + arg: "0x00", wantErr: true, }, { - arg: `"0x00112233445566778899aabbccddeeff0011223344"`, + arg: "0x00112233445566778899aabbccddeeff0011223344", wantErr: true, }, { - arg: `"""`, + arg: "invalid", wantErr: true, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &Address{} - err := v.UnmarshalJSON([]byte(tt.arg)) + v, err := AddressFromHex(tt.arg) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, tt.want, *v) + assert.Equal(t, tt.want, v) } }) } } -func Test_AddressType_Marshal(t *testing.T) { +func Test_AddressFromBytes(t *testing.T) { tests := []struct { - arg Address - want string + arg []byte + want Address + wantErr bool }{ { - arg: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), - want: `"0x00112233445566778899aabbccddeeff00112233"`, + arg: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}, + want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), }, { - arg: Address{}, - want: `"0x0000000000000000000000000000000000000000"`, + arg: []byte{}, + wantErr: true, + }, + { + arg: []byte{0x00}, + wantErr: true, + }, + { + arg: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44}, + wantErr: true, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.arg.MarshalJSON() - assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) + got, err := AddressFromBytes(tt.arg) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } }) } } -func Test_AddressType_Checksum(t *testing.T) { +func Test_AddressType_String(t *testing.T) { + addr := (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}) + assert.Equal(t, "0x00112233445566778899aabbccddeeff00112233", addr.String()) +} + +func Test_AddressType_Bytes(t *testing.T) { + addr := (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}) + expected := []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33} + assert.Equal(t, expected, addr.Bytes()) +} + +func Test_AddressType_IsZero(t *testing.T) { tests := []struct { - addr string + addr Address + isZero bool }{ - {addr: "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"}, - {addr: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}, - {addr: "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"}, - {addr: "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"}, - {addr: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"}, + {addr: Address{}, isZero: true}, + {addr: (Address)([AddressLength]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), isZero: false}, + {addr: (Address)([AddressLength]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}), isZero: false}, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - assert.Equal(t, tt.addr, MustAddressFromHex(tt.addr).Checksum(keccak256)) + assert.Equal(t, tt.isZero, tt.addr.IsZero()) }) } } -func Test_hashType_Unmarshal(t *testing.T) { +func Test_AddressType_Checksum(t *testing.T) { tests := []struct { - arg string - want Hash - wantErr bool + addr string }{ - { - arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, - want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), - }, - { - arg: `"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, - want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), - }, - { - arg: `"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"`, - wantErr: true, - }, - { - arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"`, - wantErr: true, - }, - { - arg: `"""`, - wantErr: true, - }, + {addr: "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"}, + {addr: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}, + {addr: "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"}, + {addr: "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"}, + {addr: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"}, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &Hash{} - err := v.UnmarshalJSON([]byte(tt.arg)) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, *v) - } + assert.Equal(t, tt.addr, MustAddressFromHex(tt.addr).Checksum()) }) } } -func Test_hashType_Marshal(t *testing.T) { +func Test_AddressType_MarshalJSON(t *testing.T) { tests := []struct { - arg Hash + arg Address want string }{ { - arg: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), - want: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, + arg: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), + want: `"0x00112233445566778899aabbccddeeff00112233"`, }, { - arg: Hash{}, - want: `"0x0000000000000000000000000000000000000000000000000000000000000000"`, + arg: Address{}, + want: `"0x0000000000000000000000000000000000000000"`, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.arg.MarshalJSON() + jsonBytes, err := tt.arg.MarshalJSON() assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) + assert.Equal(t, tt.want, string(jsonBytes)) }) } } -func Test_hashesType_Unmarshal(t *testing.T) { +func Test_AddressType_UnmarshalJSON(t *testing.T) { tests := []struct { arg string - want hashList + want Address wantErr bool }{ { - arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, - want: (hashList)([]Hash{{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}), + arg: `"0x00112233445566778899aabbccddeeff00112233"`, + want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), }, { - arg: `"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, - want: (hashList)([]Hash{{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}), + arg: `"00112233445566778899aabbccddeeff00112233"`, + want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), + }, + { + arg: `0x00112233445566778899aabbccddeeff00112233`, + wantErr: true, }, { - arg: `["0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"]`, - want: (hashList)([]Hash{ - {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, - {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, - }), + arg: `"0x00"`, + wantErr: true, }, { - arg: `"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"`, + arg: `"0x00112233445566778899aabbccddeeff0011223344"`, wantErr: true, }, { - arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"`, + arg: `"invalid"`, wantErr: true, }, { @@ -191,7 +191,7 @@ func Test_hashesType_Unmarshal(t *testing.T) { } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &hashList{} + v := &Address{} err := v.UnmarshalJSON([]byte(tt.arg)) if tt.wantErr { assert.Error(t, err) @@ -203,76 +203,64 @@ func Test_hashesType_Unmarshal(t *testing.T) { } } -func Test_hashesType_Marshal(t *testing.T) { +func Test_AddressType_MarshalText(t *testing.T) { tests := []struct { - arg hashList + arg Address want string }{ { - arg: (hashList)([]Hash{ - MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", PadNone), - }), - want: `"0x1111111111111111111111111111111111111111111111111111111111111111"`, - }, - { - arg: (hashList)([]Hash{ - MustHashFromHex("0x1111111111111111111111111111111111111111111111111111111111111111", PadNone), - MustHashFromHex("0x2222222222222222222222222222222222222222222222222222222222222222", PadNone), - }), - want: `["0x1111111111111111111111111111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222222222222222222222222222"]`, + arg: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), + want: `0x00112233445566778899aabbccddeeff00112233`, }, { - arg: hashList{}, - want: `[]`, + arg: Address{}, + want: `0x0000000000000000000000000000000000000000`, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.arg.MarshalJSON() + textBytes, err := tt.arg.MarshalText() assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) + assert.Equal(t, tt.want, string(textBytes)) }) } } -func Test_AddressesType_Unmarshal(t *testing.T) { +func Test_AddressType_UnmarshalText(t *testing.T) { tests := []struct { arg string - want addressList + want Address wantErr bool }{ { - arg: `"0x00112233445566778899aabbccddeeff00112233"`, - want: (addressList)([]Address{{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}}), + arg: `0x00112233445566778899aabbccddeeff00112233`, + want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), }, { - arg: `"00112233445566778899aabbccddeeff00112233"`, - want: (addressList)([]Address{{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}}), + arg: `00112233445566778899aabbccddeeff00112233`, + want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), }, { - arg: `["0x00112233445566778899aabbccddeeff00112233", "0x00112233445566778899aabbccddeeff00112233"]`, - want: (addressList)([]Address{ - {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}, - {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}, - }), + arg: `"0x00112233445566778899aabbccddeeff00112233"`, + wantErr: true, }, { - arg: `"00112233445566778899aabbccddeeff0011223344"`, + arg: `0x00`, wantErr: true, }, { - arg: `"0x00112233445566778899aabbccddeeff0011223344"`, + arg: `0x00112233445566778899aabbccddeeff0011223344`, wantErr: true, }, { - arg: `"""`, + arg: `invalid`, wantErr: true, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &addressList{} - err := v.UnmarshalJSON([]byte(tt.arg)) + v := &Address{} + err := v.UnmarshalText([]byte(tt.arg)) if tt.wantErr { assert.Error(t, err) } else { @@ -283,296 +271,164 @@ func Test_AddressesType_Unmarshal(t *testing.T) { } } -func Test_AddressesType_Marshal(t *testing.T) { +func Test_AddressType_EncodeRLP(t *testing.T) { tests := []struct { - arg addressList - want string + addr Address + want []byte }{ { - arg: (addressList)([]Address{ - MustAddressFromHex("0x1111111111111111111111111111111111111111"), - }), - want: `"0x1111111111111111111111111111111111111111"`, - }, - { - arg: (addressList)([]Address{ - MustAddressFromHex("0x1111111111111111111111111111111111111111"), - MustAddressFromHex("0x2222222222222222222222222222222222222222"), - }), - want: `["0x1111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222"]`, + addr: Address{}, + want: []byte{0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, }, { - arg: addressList{}, - want: `[]`, + addr: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), + want: []byte{0x94, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.arg.MarshalJSON() - assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) - }) - } -} - -func Test_BlockNumberType_Unmarshal(t *testing.T) { - tests := []struct { - arg string - want BlockNumber - wantErr bool - isTag bool - isEarliest bool - isLatest bool - isPending bool - isSafe bool - isFinalized bool - }{ - {arg: `"0x0"`, want: BlockNumberFromUint64(0)}, - {arg: `"0xF"`, want: BlockNumberFromUint64(15)}, - {arg: `"0"`, want: BlockNumberFromUint64(0)}, - {arg: `"F"`, want: BlockNumberFromUint64(15)}, - {arg: `"earliest"`, want: EarliestBlockNumber, isTag: true, isEarliest: true}, - {arg: `"latest"`, want: LatestBlockNumber, isTag: true, isLatest: true}, - {arg: `"pending"`, want: PendingBlockNumber, isTag: true, isPending: true}, - {arg: `"safe"`, want: SafeBlockNumber, isTag: true, isSafe: true}, - {arg: `"finalized"`, want: FinalizedBlockNumber, isTag: true, isFinalized: true}, - {arg: `"foo"`, wantErr: true}, - {arg: `"0xZ"`, wantErr: true}, - } - for n, tt := range tests { - t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &BlockNumber{} - err := v.UnmarshalJSON([]byte(tt.arg)) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, *v) - assert.Equal(t, tt.isTag, v.IsTag()) - assert.Equal(t, tt.isEarliest, v.IsEarliest()) - assert.Equal(t, tt.isLatest, v.IsLatest()) - assert.Equal(t, tt.isPending, v.IsPending()) - assert.Equal(t, tt.isSafe, v.IsSafe()) - assert.Equal(t, tt.isFinalized, v.IsFinalized()) - } - }) - } -} - -func Test_BlockNumberType_Marshal(t *testing.T) { - tests := []struct { - arg BlockNumber - want string - }{ - {arg: BlockNumberFromUint64(0), want: `"0x0"`}, - {arg: BlockNumberFromUint64(15), want: `"0xf"`}, - {arg: EarliestBlockNumber, want: `"earliest"`}, - {arg: LatestBlockNumber, want: `"latest"`}, - {arg: PendingBlockNumber, want: `"pending"`}, - {arg: SafeBlockNumber, want: `"safe"`}, - {arg: FinalizedBlockNumber, want: `"finalized"`}, - } - for n, tt := range tests { - t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.arg.MarshalJSON() + rlpBytes, err := tt.addr.EncodeRLP() assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) + assert.Equal(t, tt.want, rlpBytes) }) } } -func Test_SignatureType_Unmarshal(t *testing.T) { +func Test_AddressType_DecodeRLP(t *testing.T) { tests := []struct { - arg string - want Signature + data []byte + want Address wantErr bool }{ { - arg: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, - want: Signature{ - V: big.NewInt(0), - R: big.NewInt(0), - S: big.NewInt(0), - }, + data: []byte{0x80}, + want: Address{}, }, { - arg: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, - want: Signature{ - V: big.NewInt(0), - R: big.NewInt(0), - S: big.NewInt(0), - }, + data: []byte{0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want: Address{}, }, { - arg: `"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000021b"`, - want: Signature{ - V: big.NewInt(27), - R: big.NewInt(1), - S: big.NewInt(2), - }, + data: []byte{0x94, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}, + want: (Address)([AddressLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33}), + }, + { + data: []byte{0x81}, + wantErr: true, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &Signature{} - err := v.UnmarshalJSON([]byte(tt.arg)) + var addr Address + _, err := addr.DecodeRLP(tt.data) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) - assert.True(t, tt.want.Equal(*v)) + assert.Equal(t, tt.want, addr) } }) } } -func Test_SignatureType_Marshal(t *testing.T) { +func Test_HashFromHex(t *testing.T) { tests := []struct { - signature Signature - want string - wantErr bool + arg string + pad Pad + want Hash + wantErr bool }{ { - signature: Signature{}, - want: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, - }, - { - signature: Signature{ - V: big.NewInt(0), - R: big.NewInt(0), - S: big.NewInt(0), - }, - want: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, + arg: "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + pad: PadNone, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), }, { - signature: Signature{ - V: big.NewInt(27), - R: big.NewInt(1), - S: big.NewInt(2), - }, - want: `"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000021b"`, + arg: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + pad: PadNone, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), }, - } - for n, tt := range tests { - t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.signature.MarshalJSON() - assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) - }) - } -} - -func Test_SignatureType_Equal(t *testing.T) { - tests := []struct { - a, b Signature - want bool - }{ { - a: Signature{}, - b: Signature{}, - want: true, + arg: "80ff", + pad: PadLeft, + want: (Hash)([HashLength]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff}), }, { - a: Signature{}, - b: Signature{ - V: big.NewInt(0), - R: big.NewInt(0), - S: big.NewInt(0), - }, - want: true, + arg: "80ff", + pad: PadRight, + want: (Hash)([HashLength]byte{0x80, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), }, { - a: Signature{ - V: big.NewInt(0), - R: nil, - S: big.NewInt(0), - }, - b: Signature{ - V: nil, - R: big.NewInt(0), - S: big.NewInt(0), - }, - want: true, + arg: "0x00", + pad: PadNone, + wantErr: true, }, { - a: Signature{ - V: big.NewInt(27), - R: big.NewInt(1), - S: big.NewInt(2), - }, - b: Signature{ - V: big.NewInt(27), - R: big.NewInt(1), - S: big.NewInt(2), - }, - want: true, + arg: "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + pad: PadNone, + wantErr: true, }, { - a: Signature{ - V: big.NewInt(27), - R: nil, - S: big.NewInt(2), - }, - b: Signature{ - V: nil, - R: big.NewInt(2), - S: big.NewInt(2), - }, - want: false, + arg: "invalid", + pad: PadLeft, + wantErr: true, }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - assert.Equal(t, tt.want, tt.a.Equal(tt.b)) + v, err := HashFromHex(tt.arg, tt.pad) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, v) + } }) } } -func Test_BytesType_Unmarshal(t *testing.T) { +func Test_HashFromBytes(t *testing.T) { tests := []struct { - arg string - want Bytes + arg []byte + pad Pad + want Hash wantErr bool }{ - {arg: `"0xDEADBEEF"`, want: (Bytes)([]byte{0xDE, 0xAD, 0xBE, 0xEF})}, - {arg: `"DEADBEEF"`, want: (Bytes)([]byte{0xDE, 0xAD, 0xBE, 0xEF})}, - {arg: `"0x"`, want: (Bytes)([]byte{})}, - {arg: `""`, want: (Bytes)([]byte{})}, - {arg: `"0x0"`, want: (Bytes)([]byte{0x0})}, - {arg: `"foo"`, wantErr: true}, - {arg: `"0xZZ"`, wantErr: true}, + { + arg: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, + pad: PadNone, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + wantErr: false, + }, + { + arg: []byte{0x80, 0xff}, + pad: PadLeft, + want: (Hash)([HashLength]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff}), + }, + { + arg: []byte{0x80, 0xff}, + pad: PadRight, + want: (Hash)([HashLength]byte{0x80, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), + }, + { + arg: []byte{0x00, 0x11, 0x22}, + pad: PadNone, + wantErr: true, + }, } for n, tt := range tests { t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - v := &Bytes{} - err := v.UnmarshalJSON([]byte(tt.arg)) + got, err := HashFromBytes(tt.arg, tt.pad) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, tt.want, *v) + assert.Equal(t, tt.want, got) } }) } } -func Test_BytesType_Marshal(t *testing.T) { - tests := []struct { - arg Bytes - want string - }{ - {arg: (Bytes)([]byte{0xDE, 0xAD, 0xBE, 0xEF}), want: `"0xdeadbeef"`}, - {arg: (Bytes)([]byte{}), want: `"0x"`}, - } - for n, tt := range tests { - t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { - j, err := tt.arg.MarshalJSON() - assert.NoError(t, err) - assert.Equal(t, tt.want, string(j)) - }) - } -} - func Test_HashFromBigInt(t *testing.T) { tests := []struct { i *big.Int @@ -585,7 +441,7 @@ func Test_HashFromBigInt(t *testing.T) { }, { i: big.NewInt(1), - want: Hash{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + want: Hash{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, }, { i: big.NewInt(-1), @@ -599,7 +455,7 @@ func Test_HashFromBigInt(t *testing.T) { // min int256 { i: new(big.Int).Lsh(big.NewInt(-1), uint(255)), - want: Hash{0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + want: Hash{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, }, // max uint256 + 1 { @@ -624,10 +480,1169 @@ func Test_HashFromBigInt(t *testing.T) { } } -func keccak256(data ...[]byte) Hash { - h := sha3.NewLegacyKeccak256() - for _, i := range data { - h.Write(i) +func Test_HashType_String(t *testing.T) { + hash := (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) + assert.Equal(t, "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", hash.String()) +} + +func Test_HashType_Bytes(t *testing.T) { + hash := (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}) + expected := []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff} + assert.Equal(t, expected, hash.Bytes()) +} + +func Test_HashType_IsZero(t *testing.T) { + tests := []struct { + hash Hash + isZero bool + }{ + {hash: Hash{}, isZero: true}, + {hash: (Hash)([HashLength]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}), isZero: false}, + {hash: (Hash)([HashLength]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), isZero: false}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.isZero, tt.hash.IsZero()) + }) + } +} + +func Test_HashType_MarshalJSON(t *testing.T) { + tests := []struct { + arg Hash + want string + }{ + { + arg: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + want: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, + }, + { + arg: Hash{}, + want: `"0x0000000000000000000000000000000000000000000000000000000000000000"`, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + jsonBytes, err := tt.arg.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, tt.want, string(jsonBytes)) + }) + } +} + +func Test_HashType_UnmarshalJSON(t *testing.T) { + tests := []struct { + arg string + want Hash + wantErr bool + }{ + { + arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + }, + { + arg: `"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + }, + { + arg: `0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff`, + wantErr: true, + }, + { + arg: `"0x00"`, + wantErr: true, + }, + { + arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"`, + wantErr: true, + }, + { + arg: `"invalid"`, + wantErr: true, + }, + { + arg: `"""`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + v := &Hash{} + err := v.UnmarshalJSON([]byte(tt.arg)) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, *v) + } + }) + } +} + +func Test_HashType_MarshalText(t *testing.T) { + tests := []struct { + arg Hash + want string + }{ + { + arg: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + want: `0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff`, + }, + { + arg: Hash{}, + want: `0x0000000000000000000000000000000000000000000000000000000000000000`, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + textBytes, err := tt.arg.MarshalText() + assert.NoError(t, err) + assert.Equal(t, tt.want, string(textBytes)) + }) + } +} + +func Test_HashType_UnmarshalText(t *testing.T) { + tests := []struct { + arg string + want Hash + wantErr bool + }{ + { + arg: `0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff`, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + }, + { + arg: `00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff`, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + }, + { + arg: `"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"`, + wantErr: true, + }, + { + arg: `0x00`, + wantErr: true, + }, + { + arg: `0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00`, + wantErr: true, + }, + { + arg: `invalid`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + v := &Hash{} + err := v.UnmarshalText([]byte(tt.arg)) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, *v) + } + }) + } +} + +func Test_HashType_EncodeRLP(t *testing.T) { + tests := []struct { + hash Hash + want []byte + }{ + { + hash: Hash{}, + want: []byte{0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + }, + { + hash: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + want: []byte{0xa0, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + rlpBytes, err := tt.hash.EncodeRLP() + assert.NoError(t, err) + assert.Equal(t, tt.want, rlpBytes) + }) + } +} + +func Test_HashType_DecodeRLP(t *testing.T) { + tests := []struct { + data []byte + want Hash + wantErr bool + }{ + { + data: []byte{0x80}, + want: Hash{}, + }, + { + data: []byte{0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + want: Hash{}, + }, + { + data: []byte{0xa0, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, + want: (Hash)([HashLength]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}), + }, + { + data: []byte{0x81}, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var hash Hash + _, err := hash.DecodeRLP(tt.data) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, hash) + } + }) + } +} + +func Test_BlockNumberFromHex(t *testing.T) { + tests := []struct { + arg string + want BlockNumber + wantErr bool + }{ + {arg: "0x0", want: BlockNumberFromUint64(0)}, + {arg: "0xf", want: BlockNumberFromUint64(15)}, + {arg: "earliest", want: EarliestBlockNumber}, + {arg: "latest", want: LatestBlockNumber}, + {arg: "pending", want: PendingBlockNumber}, + {arg: "safe", want: SafeBlockNumber}, + {arg: "finalized", want: FinalizedBlockNumber}, + {arg: "foo", wantErr: true}, + {arg: "0xZZ", wantErr: true}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + bn, err := BlockNumberFromHex(tt.arg) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, bn) + } + }) + } +} + +func Test_BlockNumberType_Big(t *testing.T) { + tests := []struct { + arg BlockNumber + want *big.Int + }{ + {arg: BlockNumberFromUint64(0), want: big.NewInt(0)}, + {arg: BlockNumberFromUint64(15), want: big.NewInt(15)}, + {arg: EarliestBlockNumber, want: big.NewInt(-1)}, + {arg: LatestBlockNumber, want: big.NewInt(-2)}, + {arg: PendingBlockNumber, want: big.NewInt(-3)}, + {arg: SafeBlockNumber, want: big.NewInt(-4)}, + {arg: FinalizedBlockNumber, want: big.NewInt(-5)}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.True(t, tt.arg.Big().Cmp(tt.want) == 0) + }) + } +} + +func Test_BlockNumberType_String(t *testing.T) { + tests := []struct { + arg BlockNumber + want string + }{ + {arg: BlockNumberFromUint64(0), want: `0x0`}, + {arg: BlockNumberFromUint64(15), want: `0xf`}, + {arg: EarliestBlockNumber, want: `earliest`}, + {arg: LatestBlockNumber, want: `latest`}, + {arg: PendingBlockNumber, want: `pending`}, + {arg: SafeBlockNumber, want: `safe`}, + {arg: FinalizedBlockNumber, want: `finalized`}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg.String()) + }) + } +} + +func Test_BlockNumberType_MarshalJSON(t *testing.T) { + tests := []struct { + arg BlockNumber + want string + }{ + {arg: BlockNumberFromUint64(0), want: `"0x0"`}, + {arg: BlockNumberFromUint64(15), want: `"0xf"`}, + {arg: EarliestBlockNumber, want: `"earliest"`}, + {arg: LatestBlockNumber, want: `"latest"`}, + {arg: PendingBlockNumber, want: `"pending"`}, + {arg: SafeBlockNumber, want: `"safe"`}, + {arg: FinalizedBlockNumber, want: `"finalized"`}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + jsonBytes, err := tt.arg.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, tt.want, string(jsonBytes)) + }) + } +} + +func Test_BlockNumberType_UnmarshalJSON(t *testing.T) { + tests := []struct { + arg string + want BlockNumber + wantErr bool + isTag bool + isEarliest bool + isLatest bool + isPending bool + isSafe bool + isFinalized bool + }{ + {arg: `"0x0"`, want: BlockNumberFromUint64(0)}, + {arg: `"0xF"`, want: BlockNumberFromUint64(15)}, + {arg: `"0"`, want: BlockNumberFromUint64(0)}, + {arg: `"F"`, want: BlockNumberFromUint64(15)}, + {arg: `"earliest"`, want: EarliestBlockNumber, isTag: true, isEarliest: true}, + {arg: `"latest"`, want: LatestBlockNumber, isTag: true, isLatest: true}, + {arg: `"pending"`, want: PendingBlockNumber, isTag: true, isPending: true}, + {arg: `"safe"`, want: SafeBlockNumber, isTag: true, isSafe: true}, + {arg: `"finalized"`, want: FinalizedBlockNumber, isTag: true, isFinalized: true}, + {arg: `"foo"`, wantErr: true}, + {arg: `"0xZZ"`, wantErr: true}, + {arg: `0x0`, wantErr: true}, + {arg: `latest`, wantErr: true}, + {arg: `"""`, wantErr: true}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + v := &BlockNumber{} + err := v.UnmarshalJSON([]byte(tt.arg)) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, *v) + assert.Equal(t, tt.isTag, v.IsTag()) + assert.Equal(t, tt.isEarliest, v.IsEarliest()) + assert.Equal(t, tt.isLatest, v.IsLatest()) + assert.Equal(t, tt.isPending, v.IsPending()) + assert.Equal(t, tt.isSafe, v.IsSafe()) + assert.Equal(t, tt.isFinalized, v.IsFinalized()) + } + }) + } +} + +func Test_BlockNumberType_MarshalText(t *testing.T) { + tests := []struct { + arg BlockNumber + want string + }{ + {arg: BlockNumberFromUint64(0), want: `0x0`}, + {arg: BlockNumberFromUint64(15), want: `0xf`}, + {arg: EarliestBlockNumber, want: `earliest`}, + {arg: LatestBlockNumber, want: `latest`}, + {arg: PendingBlockNumber, want: `pending`}, + {arg: SafeBlockNumber, want: `safe`}, + {arg: FinalizedBlockNumber, want: `finalized`}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + textBytes, err := tt.arg.MarshalText() + assert.NoError(t, err) + assert.Equal(t, tt.want, string(textBytes)) + }) + } +} + +func Test_BlockNumberType_UnmarshalText(t *testing.T) { + tests := []struct { + arg string + want BlockNumber + wantErr bool + isTag bool + isEarliest bool + isLatest bool + isPending bool + isSafe bool + isFinalized bool + }{ + {arg: `0x0`, want: BlockNumberFromUint64(0)}, + {arg: `0xF`, want: BlockNumberFromUint64(15)}, + {arg: `0`, want: BlockNumberFromUint64(0)}, + {arg: `F`, want: BlockNumberFromUint64(15)}, + {arg: `earliest`, want: EarliestBlockNumber, isTag: true, isEarliest: true}, + {arg: `latest`, want: LatestBlockNumber, isTag: true, isLatest: true}, + {arg: `pending`, want: PendingBlockNumber, isTag: true, isPending: true}, + {arg: `safe`, want: SafeBlockNumber, isTag: true, isSafe: true}, + {arg: `finalized`, want: FinalizedBlockNumber, isTag: true, isFinalized: true}, + {arg: `foo`, wantErr: true}, + {arg: `0xZZ`, wantErr: true}, + {arg: `"0x0"`, wantErr: true}, + {arg: `"latest"`, wantErr: true}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + v := &BlockNumber{} + err := v.UnmarshalText([]byte(tt.arg)) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, *v) + assert.Equal(t, tt.isTag, v.IsTag()) + assert.Equal(t, tt.isEarliest, v.IsEarliest()) + assert.Equal(t, tt.isLatest, v.IsLatest()) + assert.Equal(t, tt.isPending, v.IsPending()) + assert.Equal(t, tt.isSafe, v.IsSafe()) + assert.Equal(t, tt.isFinalized, v.IsFinalized()) + } + }) + } +} + +var testSignature = Signature{ + V: func() *big.Int { + v, _ := new(big.Int).SetString("37", 10) + return v + }(), + R: func() *big.Int { + v, _ := new(big.Int).SetString("18515461264373351373200002665853028612451056578545711640558177340181847433846", 10) + return v + }(), + S: func() *big.Int { + v, _ := new(big.Int).SetString("46948507304638947509940763649030358759909902576025900602547168820602576006531", 10) + return v + }(), +} + +func Test_SignatureFromHex(t *testing.T) { + tests := []struct { + arg string + want Signature + wantErr bool + }{ + { + arg: "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325", + want: testSignature, + }, + { + arg: "0x00", + wantErr: true, + }, + { + arg: "invalid", + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + got, err := SignatureFromHex(tt.arg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_SignatureFromBytes(t *testing.T) { + tests := []struct { + arg string + want Signature + wantErr bool + }{ + { + arg: "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325", + want: testSignature, + }, + { + arg: "0x00", + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + got, err := SignatureFromBytes(hexutil.MustHexToBytes(tt.arg)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_SignatureType_String(t *testing.T) { + assert.Equal(t, "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325", testSignature.String()) +} + +func Test_SignatureType_Bytes(t *testing.T) { + assert.Equal(t, "0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325", hexutil.BytesToHex(testSignature.Bytes())) +} + +func Test_SignatureType_IsZero(t *testing.T) { + tests := []struct { + arg Signature + want bool + }{ + {arg: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(0)}, want: true}, + {arg: Signature{V: nil, R: nil, S: nil}, want: true}, + {arg: Signature{V: nil, R: big.NewInt(0), S: big.NewInt(0)}, want: true}, + {arg: Signature{V: big.NewInt(0), R: nil, S: nil}, want: true}, + {arg: Signature{V: big.NewInt(0), R: big.NewInt(0), S: nil}, want: true}, + {arg: Signature{V: big.NewInt(1), R: big.NewInt(0), S: big.NewInt(0)}, want: false}, + {arg: Signature{V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(0)}, want: false}, + {arg: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(1)}, want: false}, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg.IsZero()) + }) + } +} + +func Test_SignatureType_Equal(t *testing.T) { + tests := []struct { + arg1 Signature + arg2 Signature + want bool + }{ + { + arg1: Signature{V: big.NewInt(1), R: big.NewInt(2), S: big.NewInt(3)}, + arg2: Signature{V: big.NewInt(1), R: big.NewInt(2), S: big.NewInt(3)}, + want: true, + }, + { + arg1: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(0)}, + arg2: Signature{V: nil, R: nil, S: nil}, + want: true, + }, + { + arg1: Signature{V: big.NewInt(1), R: big.NewInt(2), S: big.NewInt(3)}, + arg2: Signature{V: big.NewInt(1), R: big.NewInt(2), S: big.NewInt(4)}, + want: false, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg1.Equal(tt.arg2)) + }) + } +} + +func Test_SignatureType_MarshalJSON(t *testing.T) { + tests := []struct { + arg Signature + want string + }{ + { + arg: testSignature, + want: `"0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325"`, + }, + { + arg: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(0)}, + want: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, + }, + { + arg: Signature{V: nil, R: nil, S: nil}, + want: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + jsonBytes, err := tt.arg.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, tt.want, string(jsonBytes)) + }) + } +} + +func Test_SignatureType_UnmarshalJSON(t *testing.T) { + tests := []struct { + arg string + want Signature + wantErr bool + }{ + { + arg: `"0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325"`, + want: testSignature, + }, + { + arg: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, + want: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(0)}, + wantErr: false, + }, + { + arg: `0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`, + wantErr: true, + }, + { + arg: `"0x00"`, + wantErr: true, + }, + { + arg: `"invalid"`, + wantErr: true, + }, + { + arg: `"""`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var sig Signature + err := sig.UnmarshalJSON([]byte(tt.arg)) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.True(t, tt.want.Equal(sig)) + } + }) + } +} + +func Test_SignatureType_MarshalText(t *testing.T) { + tests := []struct { + arg Signature + want string + }{ + { + arg: testSignature, + want: `0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325`, + }, + { + arg: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(0)}, + want: `0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + textBytes, err := tt.arg.MarshalText() + assert.NoError(t, err) + assert.Equal(t, tt.want, string(textBytes)) + }) + } +} + +func Test_SignatureType_UnmarshalText(t *testing.T) { + tests := []struct { + arg string + want Signature + wantErr bool + }{ + { + arg: `0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d8325`, + want: testSignature, + }, + { + arg: `0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`, + want: Signature{V: big.NewInt(0), R: big.NewInt(0), S: big.NewInt(0)}, + wantErr: false, + }, + { + arg: `"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"`, + wantErr: true, + }, + { + arg: `0x00`, + wantErr: true, + }, + { + arg: `invalid`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var sig Signature + err := sig.UnmarshalText([]byte(tt.arg)) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.True(t, tt.want.Equal(sig)) + } + }) + } +} + +func Test_NumberFromHex(t *testing.T) { + tests := []struct { + arg string + want Number + wantErr bool + }{ + { + arg: "0x0", + want: Number{x: *big.NewInt(0)}, + }, + { + arg: "0x10", + want: Number{x: *big.NewInt(0x10)}, + }, + { + arg: "0", + want: Number{x: *big.NewInt(0)}, + }, + { + arg: "10", + want: Number{x: *big.NewInt(0x10)}, + }, + { + arg: "-0x10", + want: Number{x: *big.NewInt(-0x10)}, + }, + { + arg: "-10", + want: Number{x: *big.NewInt(-0x10)}, + }, + { + arg: "invalid", + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + got, err := NumberFromHex(tt.arg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_NumberType_Big(t *testing.T) { + tests := []struct { + arg Number + want *big.Int + }{ + { + arg: Number{x: *big.NewInt(0)}, + want: big.NewInt(0), + }, + { + arg: Number{x: *big.NewInt(0x10)}, + want: big.NewInt(0x10), + }, + { + arg: Number{x: *big.NewInt(-0x10)}, + want: big.NewInt(-0x10), + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want.Cmp(tt.arg.Big()), 0) + }) + } +} + +func Test_NumberType_String(t *testing.T) { + tests := []struct { + arg Number + want string + }{ + { + arg: Number{x: *big.NewInt(0)}, + want: "0x0", + }, + { + arg: Number{x: *big.NewInt(0x10)}, + want: "0x10", + }, + { + arg: Number{x: *big.NewInt(-0x10)}, + want: "-0x10", + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg.String()) + }) + } +} + +func Test_NumberType_Bytes(t *testing.T) { + tests := []struct { + arg Number + want []byte + }{ + { + arg: Number{x: *big.NewInt(0)}, + want: []byte{}, + }, + { + arg: Number{x: *big.NewInt(0x10)}, + want: []byte{0x10}, + }, + { + arg: Number{x: *big.NewInt(-0x10)}, + want: []byte{0x10}, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg.Bytes()) + }) + } +} + +func Test_NumberType_MarshalJSON(t *testing.T) { + tests := []struct { + arg Number + want string + }{ + { + arg: Number{x: *big.NewInt(0)}, + want: `"0x0"`, + }, + { + arg: Number{x: *big.NewInt(0x10)}, + want: `"0x10"`, + }, + { + arg: Number{x: *big.NewInt(-0x10)}, + want: `"-0x10"`, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + got, err := tt.arg.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, tt.want, string(got)) + }) + } +} + +func Test_NumberType_UnmarshalJSON(t *testing.T) { + tests := []struct { + arg string + want Number + wantErr bool + }{ + { + arg: `"0x0"`, + want: Number{x: *big.NewInt(0)}, + }, + { + arg: `"0x10"`, + want: Number{x: *big.NewInt(0x10)}, + }, + { + arg: `"-10"`, + want: Number{x: *big.NewInt(-0x10)}, + }, + { + arg: `"10"`, + want: Number{x: *big.NewInt(0x10)}, + }, + { + arg: `0x10"`, + wantErr: true, + }, + { + arg: `10"`, + wantErr: true, + }, + { + arg: `"invalid"`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var got Number + err := got.UnmarshalJSON([]byte(tt.arg)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_NumberType_MarshalText(t *testing.T) { + tests := []struct { + arg Number + want string + }{ + { + arg: Number{x: *big.NewInt(0)}, + want: `0x0`, + }, + { + arg: Number{x: *big.NewInt(0x10)}, + want: `0x10`, + }, + { + arg: Number{x: *big.NewInt(-0x10)}, + want: `-0x10`, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + textBytes, err := tt.arg.MarshalText() + require.NoError(t, err) + assert.Equal(t, tt.want, string(textBytes)) + }) + } +} + +func Test_NumberType_UnmarshalText(t *testing.T) { + tests := []struct { + arg string + want Number + wantErr bool + }{ + { + arg: `0x0`, + want: Number{x: *big.NewInt(0)}, + }, + { + arg: `0x10`, + want: Number{x: *big.NewInt(0x10)}, + }, + { + arg: `-10`, + want: Number{x: *big.NewInt(-0x10)}, + }, + { + arg: `10`, + want: Number{x: *big.NewInt(0x10)}, + }, + { + arg: `"0x10"`, + wantErr: true, + }, + { + arg: `"10"`, + wantErr: true, + }, + { + arg: `invalid`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var got Number + err := got.UnmarshalText([]byte(tt.arg)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_BytesFromHex(t *testing.T) { + tests := []struct { + arg string + want Bytes + wantErr bool + }{ + { + arg: "0x00112233", + want: Bytes{0x00, 0x11, 0x22, 0x33}, + }, + { + arg: "0x00112233", + want: Bytes{0x00, 0x11, 0x22, 0x33}, + }, + { + arg: "invalid", + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + got, err := BytesFromHex(tt.arg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_Bytes_PadLeft(t *testing.T) { + tests := []struct { + arg Bytes + len int + want Bytes + }{ + { + arg: Bytes{0x01, 0x02}, + len: 0, + want: Bytes{}, + }, + { + arg: Bytes{0x01, 0x02}, + len: 1, + want: Bytes{0x02}, + }, + { + arg: Bytes{0x01, 0x02}, + len: 4, + want: Bytes{0x00, 0x00, 0x01, 0x02}, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg.PadLeft(tt.len)) + }) + } +} + +func Test_Bytes_PadRight(t *testing.T) { + tests := []struct { + arg Bytes + len int + want Bytes + }{ + { + arg: Bytes{0x01, 0x02}, + len: 0, + want: Bytes{}, + }, + { + arg: Bytes{0x01, 0x02}, + len: 1, + want: Bytes{0x01}, + }, + { + arg: Bytes{0x01, 0x02}, + len: 4, + want: Bytes{0x01, 0x02, 0x00, 0x00}, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + assert.Equal(t, tt.want, tt.arg.PadRight(tt.len)) + }) + } +} + +func Test_Bytes_Bytes(t *testing.T) { + b := Bytes{0x01, 0x02, 0x03} + assert.Equal(t, []byte{0x01, 0x02, 0x03}, b.Bytes()) +} + +func Test_Bytes_String(t *testing.T) { + b := Bytes{0x01, 0x02, 0x03} + assert.Equal(t, "0x010203", b.String()) +} + +func Test_Bytes_MarshalJSON(t *testing.T) { + b := Bytes{0x01, 0x02, 0x03} + data, err := b.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, `"0x010203"`, string(data)) +} + +func Test_Bytes_UnmarshalJSON(t *testing.T) { + tests := []struct { + arg string + want Bytes + wantErr bool + }{ + { + arg: `"0x010203"`, + want: Bytes{0x01, 0x02, 0x03}, + }, + { + arg: `"010203"`, + want: Bytes{0x01, 0x02, 0x03}, + }, + { + arg: `0x010203`, + wantErr: true, + }, + { + arg: `"invalid"`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var b Bytes + err := b.UnmarshalJSON([]byte(tt.arg)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, b) + } + }) + } +} + +func Test_Bytes_MarshalText(t *testing.T) { + b := Bytes{0x01, 0x02, 0x03} + data, err := b.MarshalText() + require.NoError(t, err) + assert.Equal(t, "0x010203", string(data)) +} + +func Test_Bytes_UnmarshalText(t *testing.T) { + tests := []struct { + arg string + want Bytes + wantErr bool + }{ + { + arg: `0x010203`, + want: Bytes{0x01, 0x02, 0x03}, + }, + { + arg: `010203`, + want: Bytes{0x01, 0x02, 0x03}, + }, + { + arg: `"0x010203"`, + wantErr: true, + }, + { + arg: `invalid`, + wantErr: true, + }, + } + for n, tt := range tests { + t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) { + var b Bytes + err := b.UnmarshalText([]byte(tt.arg)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, b) + } + }) } - return MustHashFromBytes(h.Sum(nil), PadNone) } diff --git a/types/units.go b/types/units.go new file mode 100644 index 0000000..668f265 --- /dev/null +++ b/types/units.go @@ -0,0 +1,7 @@ +package types + +const ( + WeiDenomination = 1 + GWeiDenomination = 1e9 + EtherDenomination = 1e18 +) diff --git a/types/util.go b/types/util.go index f55e0b3..b85b4a2 100644 --- a/types/util.go +++ b/types/util.go @@ -2,9 +2,12 @@ package types import ( "bytes" + "encoding/json" "fmt" "math/big" + "github.com/defiweb/go-rlp" + "github.com/defiweb/go-eth/hexutil" ) @@ -29,7 +32,11 @@ func bytesUnmarshalJSON(input []byte, output *[]byte) error { if bytes.Equal(input, []byte("null")) { return nil } - return bytesUnmarshalText(naiveUnquote(input), output) + input, ok := naiveUnquote(input) + if !ok { + return fmt.Errorf("invalid JSON string: %s", input) + } + return bytesUnmarshalText(input, output) } // bytesUnmarshalText decodes the given string where each byte is represented by @@ -48,7 +55,11 @@ func fixedBytesUnmarshalJSON(input, output []byte) error { if bytes.Equal(input, []byte("null")) { return nil } - return fixedBytesUnmarshalText(naiveUnquote(input), output) + input, ok := naiveUnquote(input) + if !ok { + return fmt.Errorf("invalid JSON string: %s", input) + } + return fixedBytesUnmarshalText(input, output) } // fixedBytesUnmarshalText works like bytesUnmarshalText, but it is designed to @@ -66,6 +77,24 @@ func fixedBytesUnmarshalText(input, output []byte) error { return nil } +// fixedBytesDecodeRLP decodes the given RLP encoded data into the given byte +// slice. The input data must be exactly the same length as the output slice. +func fixedBytesDecodeRLP(input []byte, output []byte) (int, error) { + r, n, err := rlp.DecodeLazy(input) + if err != nil { + return n, err + } + b, err := r.Bytes() + if err != nil { + return n, err + } + if len(b) != len(output) { + return n, fmt.Errorf("invalid length %d", len(b)) + } + copy(output, b) + return n, nil +} + // numberMarshalJSON encodes the given big integer as JSON string where number // is resented in hexadecimal format. The hex string is prefixed with "0x". // Negative numbers are prefixed with "-0x". @@ -84,7 +113,11 @@ func numberMarshalText(input *big.Int) []byte { // hexadecimal format. The hex string may be prefixed with "0x". Negative numbers // must start with minus sign. func numberUnmarshalJSON(input []byte, output *big.Int) error { - return numberUnmarshalText(naiveUnquote(input), output) + input, ok := naiveUnquote(input) + if !ok { + return fmt.Errorf("invalid JSON string: %s", input) + } + return numberUnmarshalText(input, output) } // numberUnmarshalText decodes the given string where number is resented in @@ -99,6 +132,30 @@ func numberUnmarshalText(input []byte, output *big.Int) error { return nil } +// marshalJSONMerge marshals given values into a single JSON object. +// The given values must marshal into JSON objects. If same field is present in +// multiple values, both are included in the result resulting in an invalid +// JSON object. +func marshalJSONMerge(vs ...any) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('{') + for n, v := range vs { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + if len(b) < 2 || (b[0] != '{' && b[len(b)-1] != '}') { + return nil, fmt.Errorf("expected JSON object, got %s", b) + } + if n > 0 { + buf.WriteByte(',') + } + buf.Write(b[1 : len(b)-1]) + } + buf.WriteByte('}') + return buf.Bytes(), nil +} + // naiveQuote returns a double-quoted string. It does not perform any escaping. func naiveQuote(i []byte) []byte { b := make([]byte, len(i)+2) @@ -110,9 +167,40 @@ func naiveQuote(i []byte) []byte { // naiveUnquote returns the string inside the quotes. It does not perform any // unescaping. -func naiveUnquote(i []byte) []byte { +func naiveUnquote(i []byte) ([]byte, bool) { if len(i) >= 2 && i[0] == '"' && i[len(i)-1] == '"' { - return i[1 : len(i)-1] + return i[1 : len(i)-1], true + } + return i, false +} + +// copyPtr copies the value of the given pointer and returns a new pointer to +// it. If the given pointer is nil, it returns nil. +func copyPtr[T any](p *T) *T { + if p == nil { + return nil + } + c := *p + return &c +} + +// copyBytes copies the given byte slice and returns a new slice. If the given +// slice is nil, it returns nil. +func copyBytes(p []byte) []byte { + if p == nil { + return nil + } + c := make([]byte, len(p)) + copy(c, p) + return c +} + +// copyBigInt copies the given big integer and returns a new big integer. If the +// given big integer is nil, it returns nil. +func copyBigInt(p *big.Int) *big.Int { + if p == nil { + return nil } - return i + c := new(big.Int).Set(p) + return c } diff --git a/types/util_test.go b/types/util_test.go new file mode 100644 index 0000000..a817116 --- /dev/null +++ b/types/util_test.go @@ -0,0 +1,58 @@ +package types + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func assertEqualTX(t *testing.T, actual, expected Transaction) { + assert.Equal(t, deref(reflect.TypeOf(actual)), deref(reflect.TypeOf(expected))) + assert.Equal(t, actual.GetTransactionData(), expected.GetTransactionData()) + if _, ok := expected.(HasCallData); ok { + assert.Equal(t, actual.(HasCallData).GetCallData(), actual.(HasCallData).GetCallData()) + } + if _, ok := actual.(HasLegacyPriceData); ok { + assert.Equal(t, expected.(HasLegacyPriceData).GetLegacyPriceData(), actual.(HasLegacyPriceData).GetLegacyPriceData()) + } + if _, ok := actual.(HasAccessListData); ok { + assert.Equal(t, expected.(HasAccessListData).GetAccessListData(), actual.(HasAccessListData).GetAccessListData()) + } + if _, ok := actual.(HasDynamicFeeData); ok { + assert.Equal(t, expected.(HasDynamicFeeData).GetDynamicFeeData(), actual.(HasDynamicFeeData).GetDynamicFeeData()) + } + if _, ok := actual.(HasBlobData); ok { + assert.Equal(t, expected.(HasBlobData).GetBlobData(), actual.(HasBlobData).GetBlobData()) + } +} + +func assertEqualCall(t *testing.T, actual, expected Call) { + assert.Equal(t, deref(reflect.TypeOf(actual)), deref(reflect.TypeOf(expected))) + if _, ok := expected.(HasCallData); ok { + assert.Equal(t, actual.(HasCallData).GetCallData(), actual.(HasCallData).GetCallData()) + } + if _, ok := actual.(HasLegacyPriceData); ok { + assert.Equal(t, expected.(HasLegacyPriceData).GetLegacyPriceData(), actual.(HasLegacyPriceData).GetLegacyPriceData()) + } + if _, ok := actual.(HasAccessListData); ok { + assert.Equal(t, expected.(HasAccessListData).GetAccessListData(), actual.(HasAccessListData).GetAccessListData()) + } + if _, ok := actual.(HasDynamicFeeData); ok { + assert.Equal(t, expected.(HasDynamicFeeData).GetDynamicFeeData(), actual.(HasDynamicFeeData).GetDynamicFeeData()) + } + if _, ok := actual.(HasBlobData); ok { + assert.Equal(t, expected.(HasBlobData).GetBlobData(), actual.(HasBlobData).GetBlobData()) + } +} + +func deref(t reflect.Type) reflect.Type { + for t.Kind() == reflect.Ptr || t.Kind() == reflect.Interface { + t = t.Elem() + } + return t +} + +func ptr[T any](x T) *T { + return &x +} diff --git a/wallet/key.go b/wallet/key.go index 74e8f85..e67af44 100644 --- a/wallet/key.go +++ b/wallet/key.go @@ -15,7 +15,7 @@ type Key interface { SignMessage(ctx context.Context, data []byte) (*types.Signature, error) // SignTransaction signs the given transaction. - SignTransaction(ctx context.Context, tx *types.Transaction) error + SignTransaction(ctx context.Context, tx types.Transaction) error // VerifyMessage verifies whether the given data is signed by the key. VerifyMessage(ctx context.Context, data []byte, sig types.Signature) bool diff --git a/wallet/key_hd.go b/wallet/key_hd.go index f66c27c..4200601 100644 --- a/wallet/key_hd.go +++ b/wallet/key_hd.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/btcsuite/btcd/chaincfg" + "github.com/defiweb/go-eth/crypto/ecdsa" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/tyler-smith/go-bip39" @@ -116,8 +117,7 @@ func (m Mnemonic) Derive(path DerivationPath) (*PrivateKey, error) { if err != nil { return nil, err } - privKeyECDSA := privKey.ToECDSA() - return NewKeyFromECDSA(privKeyECDSA), nil + return NewKeyFromECDSA(&ecdsa.PrivateKey{D: privKey.ToECDSA().D}), nil } // ParseDerivationPath converts a BIP-33 derivation path string into the diff --git a/wallet/key_json_v3.go b/wallet/key_json_v3.go index ddc5c5d..2baa0c3 100644 --- a/wallet/key_json_v3.go +++ b/wallet/key_json_v3.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/ecdsa" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -15,6 +14,8 @@ import ( "golang.org/x/crypto/scrypt" "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/ecdsa" + "github.com/defiweb/go-eth/types" ) // The code below is based on: @@ -70,7 +71,7 @@ func encryptV3Key(key *ecdsa.PrivateKey, passphrase string, scryptN, scryptP int return &jsonKey{ Version: 3, ID: id, - Address: crypto.ECPublicKeyToAddress(&key.PublicKey), + Address: types.Address(crypto.ECPublicKeyToAddress(crypto.ECPrivateKeyToPublicKey(key))), Crypto: jsonKeyCrypto{ Cipher: "aes-128-ctr", CipherParams: jsonKeyCipherParams{ @@ -85,7 +86,7 @@ func encryptV3Key(key *ecdsa.PrivateKey, passphrase string, scryptN, scryptP int R: scryptR, Salt: salt, }, - MAC: mac.Bytes(), + MAC: mac[:], }, }, nil } @@ -105,7 +106,7 @@ func decryptV3Key(cryptoJson jsonKeyCrypto, passphrase []byte) ([]byte, error) { // VerifyHash the derived key matches the key in the JSON. If not, the // passphrase is incorrect. calculatedMAC := crypto.Keccak256(derivedKey[16:32], cryptoJson.CipherText) - if !bytes.Equal(calculatedMAC.Bytes(), cryptoJson.MAC) { + if !bytes.Equal(calculatedMAC[:], cryptoJson.MAC) { return nil, fmt.Errorf("invalid passphrase or keyfile") } diff --git a/wallet/key_priv.go b/wallet/key_priv.go index 3c4e371..168245d 100644 --- a/wallet/key_priv.go +++ b/wallet/key_priv.go @@ -2,46 +2,41 @@ package wallet import ( "context" - "crypto/ecdsa" - "crypto/rand" "encoding/json" "github.com/btcsuite/btcd/btcec/v2" "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/ecdsa" + "github.com/defiweb/go-eth/crypto/txsign" "github.com/defiweb/go-eth/types" ) -var s256 = btcec.S256() - type PrivateKey struct { private *ecdsa.PrivateKey public *ecdsa.PublicKey address types.Address - sign crypto.Signer - recover crypto.Recoverer } // NewKeyFromECDSA creates a new private key from an ecdsa.PrivateKey. func NewKeyFromECDSA(prv *ecdsa.PrivateKey) *PrivateKey { + pub := crypto.ECPrivateKeyToPublicKey(prv) return &PrivateKey{ private: prv, - public: &prv.PublicKey, - address: crypto.ECPublicKeyToAddress(&prv.PublicKey), - sign: crypto.ECSigner(prv), - recover: crypto.ECRecoverer, + public: pub, + address: types.Address(crypto.ECPublicKeyToAddress(pub)), } } // NewKeyFromBytes creates a new private key from private key bytes. func NewKeyFromBytes(prv []byte) *PrivateKey { key, _ := btcec.PrivKeyFromBytes(prv) - return NewKeyFromECDSA(key.ToECDSA()) + return NewKeyFromECDSA(&ecdsa.PrivateKey{D: key.ToECDSA().D}) } // NewRandomKey creates a random private key. func NewRandomKey() *PrivateKey { - key, err := ecdsa.GenerateKey(s256, rand.Reader) + key, err := ecdsa.GenerateKey() if err != nil { panic(err) } @@ -74,33 +69,41 @@ func (k *PrivateKey) Address() types.Address { // SignHash implements the KeyWithHashSigner interface. func (k *PrivateKey) SignHash(_ context.Context, hash types.Hash) (*types.Signature, error) { - return k.sign.SignHash(hash) + s, err := crypto.ECSignHash(k.private, ecdsa.Hash(hash)) + if err != nil { + return nil, err + } + return (*types.Signature)(s), nil } // SignMessage implements the Key interface. func (k *PrivateKey) SignMessage(_ context.Context, data []byte) (*types.Signature, error) { - return k.sign.SignMessage(data) + s, err := crypto.ECSignMessage(k.private, data) + if err != nil { + return nil, err + } + return (*types.Signature)(s), nil } // SignTransaction implements the Key interface. -func (k *PrivateKey) SignTransaction(_ context.Context, tx *types.Transaction) error { - return k.sign.SignTransaction(tx) +func (k *PrivateKey) SignTransaction(_ context.Context, tx types.Transaction) error { + return txsign.Sign(k.private, tx) } // VerifyHash implements the KeyWithHashSigner interface. func (k *PrivateKey) VerifyHash(_ context.Context, hash types.Hash, sig types.Signature) bool { - addr, err := k.recover.RecoverHash(hash, sig) + addr, err := crypto.ECRecoverHash(ecdsa.Hash(hash), ecdsa.Signature(sig)) if err != nil { return false } - return *addr == k.address + return types.Address(*addr) == k.address } // VerifyMessage implements the Key interface. func (k *PrivateKey) VerifyMessage(_ context.Context, data []byte, sig types.Signature) bool { - addr, err := k.recover.RecoverMessage(data, sig) + addr, err := crypto.ECRecoverMessage(data, ecdsa.Signature(sig)) if err != nil { return false } - return *addr == k.address + return types.Address(*addr) == k.address } diff --git a/wallet/key_rpc.go b/wallet/key_rpc.go index 36c4851..b4ce846 100644 --- a/wallet/key_rpc.go +++ b/wallet/key_rpc.go @@ -2,8 +2,11 @@ package wallet import ( "context" + "fmt" "github.com/defiweb/go-eth/crypto" + "github.com/defiweb/go-eth/crypto/ecdsa" + "github.com/defiweb/go-eth/crypto/txsign" "github.com/defiweb/go-eth/types" ) @@ -11,14 +14,14 @@ import ( // sign messages and transactions. type RPCSigningClient interface { Sign(ctx context.Context, account types.Address, data []byte) (*types.Signature, error) - SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, *types.Transaction, error) + SignTransaction(ctx context.Context, tx types.Transaction) ([]byte, error) } // KeyRPC is an Ethereum key that uses an RPC client to sign messages and transactions. type KeyRPC struct { client RPCSigningClient address types.Address - recover crypto.Recoverer + decoder types.RLPTransactionDecoder } // NewKeyRPC returns a new KeyRPC. @@ -26,7 +29,7 @@ func NewKeyRPC(client RPCSigningClient, address types.Address) *KeyRPC { return &KeyRPC{ client: client, address: address, - recover: crypto.ECRecoverer, + decoder: types.DefaultTransactionDecoder, } } @@ -41,20 +44,31 @@ func (k *KeyRPC) SignMessage(ctx context.Context, data []byte) (*types.Signature } // SignTransaction implements the Key interface. -func (k *KeyRPC) SignTransaction(ctx context.Context, tx *types.Transaction) error { - _, signedTX, err := k.client.SignTransaction(ctx, tx) +func (k *KeyRPC) SignTransaction(ctx context.Context, tx types.Transaction) error { + raw, err := k.client.SignTransaction(ctx, tx) if err != nil { return err } - *tx = *signedTX + stx, err := k.decoder.DecodeRLP(raw) + if err != nil { + return fmt.Errorf("failed to decode signed transaction: %w", err) + } + tx.SetTransactionData(*stx.GetTransactionData()) + addr, err := txsign.Recover(tx) + if err != nil { + return fmt.Errorf("failed to verify signed transaction: %w", err) + } + if *addr != k.address { + return fmt.Errorf("failed to verify signed transaction: recovered address does not match key address") + } return err } // VerifyMessage implements the Key interface. func (k *KeyRPC) VerifyMessage(_ context.Context, data []byte, sig types.Signature) bool { - addr, err := k.recover.RecoverMessage(data, sig) + addr, err := crypto.ECRecoverMessage(data, ecdsa.Signature(sig)) if err != nil { return false } - return *addr == k.address + return types.Address(*addr) == k.address }