-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eth-rpc): Conversion types and functions between Ethereum txs an…
…d blocks and Tendermint ones.
- Loading branch information
1 parent
1a05061
commit 36730c5
Showing
11 changed files
with
1,271 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Copyright (c) 2023-2024 Nibi, Inc. | ||
package types | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
) | ||
|
||
// AddrLocker is a mutex structure used to avoid querying outdated account data | ||
type AddrLocker struct { | ||
mu sync.Mutex | ||
locks map[common.Address]*sync.Mutex | ||
} | ||
|
||
// lock returns the lock of the given address. | ||
func (l *AddrLocker) lock(address common.Address) *sync.Mutex { | ||
l.mu.Lock() | ||
defer l.mu.Unlock() | ||
if l.locks == nil { | ||
l.locks = make(map[common.Address]*sync.Mutex) | ||
} | ||
if _, ok := l.locks[address]; !ok { | ||
l.locks[address] = new(sync.Mutex) | ||
} | ||
return l.locks[address] | ||
} | ||
|
||
// LockAddr locks an account's mutex. This is used to prevent another tx getting the | ||
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from | ||
// being read again during the time that the first transaction is being signed. | ||
func (l *AddrLocker) LockAddr(address common.Address) { | ||
l.lock(address).Lock() | ||
} | ||
|
||
// UnlockAddr unlocks the mutex of the given account. | ||
func (l *AddrLocker) UnlockAddr(address common.Address) { | ||
l.lock(address).Unlock() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
// Copyright (c) 2023-2024 Nibi, Inc. | ||
package types | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"math" | ||
"math/big" | ||
"strings" | ||
|
||
"github.com/spf13/cast" | ||
"google.golang.org/grpc/metadata" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
|
||
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" | ||
|
||
"github.com/NibiruChain/nibiru/eth/types" | ||
) | ||
|
||
// BlockNumber represents a decoded hex string for a block number. | ||
type BlockNumber int64 | ||
|
||
const ( | ||
EthPendingBlockNumber = BlockNumber(-2) | ||
EthLatestBlockNumber = BlockNumber(-1) | ||
EthEarliestBlockNumber = BlockNumber(0) | ||
) | ||
|
||
const ( | ||
BlockParamEarliest = "earliest" | ||
BlockParamLatest = "latest" | ||
BlockParamFinalized = "finalized" | ||
BlockParamSafe = "safe" | ||
BlockParamPending = "pending" | ||
) | ||
|
||
// NewBlockNumber creates a new BlockNumber instance. | ||
func NewBlockNumber(n *big.Int) BlockNumber { | ||
if !n.IsInt64() { | ||
// default to latest block if it overflows | ||
return EthLatestBlockNumber | ||
} | ||
|
||
return BlockNumber(n.Int64()) | ||
} | ||
|
||
// ContextWithHeight wraps a context with the a gRPC block height header. If the | ||
// provided height is 0, it will return an empty context and the gRPC query will | ||
// use the latest block height for querying. Note that all metadata gets processed | ||
// and removed by tendermint layer, so it wont be accessible at gRPC server | ||
// level. | ||
func ContextWithHeight(height int64) context.Context { | ||
if height == 0 { | ||
return context.Background() | ||
} | ||
return metadata.AppendToOutgoingContext( | ||
context.Background(), | ||
grpctypes.GRPCBlockHeightHeader, | ||
fmt.Sprintf("%d", height), | ||
) | ||
} | ||
|
||
// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports: | ||
// - "latest", "finalized", "earliest" or "pending" as string arguments | ||
// - the block number | ||
// | ||
// Returned errors: | ||
// - Invalid block number error when the given argument isn't a known string | ||
// - Out of range error when the given block number is too large or small | ||
func (bn *BlockNumber) UnmarshalJSON(data []byte) error { | ||
input := strings.TrimSpace(string(data)) | ||
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' { | ||
input = input[1 : len(input)-1] | ||
} | ||
|
||
switch input { | ||
case BlockParamEarliest: | ||
*bn = EthEarliestBlockNumber | ||
return nil | ||
case BlockParamLatest, BlockParamFinalized, BlockParamSafe: | ||
*bn = EthLatestBlockNumber | ||
return nil | ||
case BlockParamPending: | ||
*bn = EthPendingBlockNumber | ||
return nil | ||
} | ||
|
||
blckNum, err := hexutil.DecodeUint64(input) | ||
if errors.Is(err, hexutil.ErrMissingPrefix) { | ||
blckNum = cast.ToUint64(input) | ||
} else if err != nil { | ||
return err | ||
} | ||
|
||
if blckNum > math.MaxInt64 { | ||
return fmt.Errorf("block number larger than int64") | ||
} | ||
*bn = BlockNumber(blckNum) // #nosec G701 | ||
|
||
return nil | ||
} | ||
|
||
// Int64 converts block number to primitive type. This function enforces the | ||
// first block starting from 1-index. | ||
func (bn BlockNumber) Int64() int64 { | ||
if bn < 0 { | ||
return 0 | ||
} else if bn == 0 { | ||
return 1 | ||
} | ||
|
||
return int64(bn) | ||
} | ||
|
||
// TmHeight is a util function used for the Tendermint RPC client. It returns | ||
// nil if the block number is "latest". Otherwise, it returns the pointer of the | ||
// int64 value of the height. | ||
func (bn BlockNumber) TmHeight() *int64 { | ||
if bn < 0 { | ||
return nil | ||
} | ||
|
||
height := bn.Int64() | ||
return &height | ||
} | ||
|
||
// BlockNumberOrHash represents a block number or a block hash. | ||
type BlockNumberOrHash struct { | ||
BlockNumber *BlockNumber `json:"blockNumber,omitempty"` | ||
BlockHash *common.Hash `json:"blockHash,omitempty"` | ||
} | ||
|
||
func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { | ||
type erased BlockNumberOrHash | ||
e := erased{} | ||
err := json.Unmarshal(data, &e) | ||
if err == nil { | ||
return bnh.checkUnmarshal(BlockNumberOrHash(e)) | ||
} | ||
var input string | ||
err = json.Unmarshal(data, &input) | ||
if err != nil { | ||
return err | ||
} | ||
err = bnh.decodeFromString(input) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (bnh *BlockNumberOrHash) checkUnmarshal(e BlockNumberOrHash) error { | ||
if e.BlockNumber != nil && e.BlockHash != nil { | ||
return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other") | ||
} | ||
bnh.BlockNumber = e.BlockNumber | ||
bnh.BlockHash = e.BlockHash | ||
return nil | ||
} | ||
|
||
func (bnh *BlockNumberOrHash) decodeFromString(input string) error { | ||
switch input { | ||
case BlockParamEarliest: | ||
bn := EthEarliestBlockNumber | ||
bnh.BlockNumber = &bn | ||
case BlockParamLatest, BlockParamFinalized: | ||
bn := EthLatestBlockNumber | ||
bnh.BlockNumber = &bn | ||
case BlockParamPending: | ||
bn := EthPendingBlockNumber | ||
bnh.BlockNumber = &bn | ||
default: | ||
// check if the input is a block hash | ||
if len(input) == 66 { | ||
hash := common.Hash{} | ||
err := hash.UnmarshalText([]byte(input)) | ||
if err != nil { | ||
return err | ||
} | ||
bnh.BlockHash = &hash | ||
break | ||
} | ||
// otherwise take the hex string has int64 value | ||
blockNumber, err := hexutil.DecodeUint64(input) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
bnInt, err := types.SafeInt64(blockNumber) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
bn := BlockNumber(bnInt) | ||
bnh.BlockNumber = &bn | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package types | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestUnmarshalBlockNumberOrHash(t *testing.T) { | ||
bnh := new(BlockNumberOrHash) | ||
|
||
testCases := []struct { | ||
msg string | ||
input []byte | ||
malleate func() | ||
expPass bool | ||
}{ | ||
{ | ||
"JSON input with block hash", | ||
[]byte("{\"blockHash\": \"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\"}"), | ||
func() { | ||
require.Equal(t, *bnh.BlockHash, common.HexToHash("0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739")) | ||
require.Nil(t, bnh.BlockNumber) | ||
}, | ||
true, | ||
}, | ||
{ | ||
"JSON input with block number", | ||
[]byte("{\"blockNumber\": \"0x35\"}"), | ||
func() { | ||
require.Equal(t, *bnh.BlockNumber, BlockNumber(0x35)) | ||
require.Nil(t, bnh.BlockHash) | ||
}, | ||
true, | ||
}, | ||
{ | ||
"JSON input with block number latest", | ||
[]byte("{\"blockNumber\": \"latest\"}"), | ||
func() { | ||
require.Equal(t, *bnh.BlockNumber, EthLatestBlockNumber) | ||
require.Nil(t, bnh.BlockHash) | ||
}, | ||
true, | ||
}, | ||
{ | ||
"JSON input with both block hash and block number", | ||
[]byte("{\"blockHash\": \"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\", \"blockNumber\": \"0x35\"}"), | ||
func() { | ||
}, | ||
false, | ||
}, | ||
{ | ||
"String input with block hash", | ||
[]byte("\"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\""), | ||
func() { | ||
require.Equal(t, *bnh.BlockHash, common.HexToHash("0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739")) | ||
require.Nil(t, bnh.BlockNumber) | ||
}, | ||
true, | ||
}, | ||
{ | ||
"String input with block number", | ||
[]byte("\"0x35\""), | ||
func() { | ||
require.Equal(t, *bnh.BlockNumber, BlockNumber(0x35)) | ||
require.Nil(t, bnh.BlockHash) | ||
}, | ||
true, | ||
}, | ||
{ | ||
"String input with block number latest", | ||
[]byte("\"latest\""), | ||
func() { | ||
require.Equal(t, *bnh.BlockNumber, EthLatestBlockNumber) | ||
require.Nil(t, bnh.BlockHash) | ||
}, | ||
true, | ||
}, | ||
{ | ||
"String input with block number overflow", | ||
[]byte("\"0xffffffffffffffffffffffffffffffffffffff\""), | ||
func() { | ||
}, | ||
false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
fmt.Printf("Case %s", tc.msg) | ||
// reset input | ||
bnh = new(BlockNumberOrHash) | ||
err := bnh.UnmarshalJSON(tc.input) | ||
tc.malleate() | ||
if tc.expPass { | ||
require.NoError(t, err) | ||
} else { | ||
require.Error(t, err) | ||
} | ||
} | ||
} |
Oops, something went wrong.