Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 124 additions & 3 deletions ethereum/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (ec *Client) Status(ctx context.Context) (
[]*RosettaTypes.Peer,
error,
) {
header, err := ec.blockHeader(ctx, nil)
header, err := ec.blockHeaderByNumber(ctx, nil)
if err != nil {
return nil, -1, nil, nil, err
}
Expand Down Expand Up @@ -210,6 +210,88 @@ func toBlockNumArg(number *big.Int) string {
return hexutil.EncodeBig(number)
}

// Transaction returns the transaction response of the Transaction identified
// by *RosettaTypes.TransactionIdentifier hash
func (ec *Client) Transaction(
ctx context.Context,
blockIdentifier *RosettaTypes.BlockIdentifier,
transactionIdentifier *RosettaTypes.TransactionIdentifier,
) (*RosettaTypes.Transaction, error) {
if transactionIdentifier.Hash == "" {
return nil, errors.New("transaction hash is required")
}

var raw json.RawMessage
err := ec.c.CallContext(ctx, &raw, "eth_getTransactionByHash", transactionIdentifier.Hash)
if err != nil {
return nil, fmt.Errorf("%w: transaction fetch failed", err)
} else if len(raw) == 0 {
return nil, ethereum.NotFound
}

// Decode transaction
var body rpcTransaction

if err := json.Unmarshal(raw, &body); err != nil {
return nil, err
}

var header *types.Header
if blockIdentifier.Hash != "" {
header, err = ec.blockHeaderByHash(ctx, blockIdentifier.Hash)
} else {
header, err = ec.blockHeaderByNumber(ctx, big.NewInt(blockIdentifier.Index))
}

if err != nil {
return nil, fmt.Errorf("%w: could not get block header for %x", err, blockIdentifier.Hash)
}

receipt, err := ec.transactionReceipt(ctx, body.tx.Hash())
if receipt.BlockHash != *body.BlockHash {
return nil, fmt.Errorf(
"%w: expected block hash %s for transaction but got %s",
ErrBlockOrphaned,
body.BlockHash.Hex(),
receipt.BlockHash.Hex(),
)
}
if err != nil {
return nil, fmt.Errorf("%w: could not get receipt for %x", err, body.tx.Hash())
}

var traces *Call
var rawTraces json.RawMessage
var addTraces bool
if header.Number.Int64() != GenesisBlockIndex { // not possible to get traces at genesis
addTraces = true
traces, rawTraces, err = ec.getTransactionTraces(ctx, body.tx.Hash())
if err != nil {
return nil, fmt.Errorf("%w: could not get traces for %x", err, body.tx.Hash())
}
}

gasUsedBig := new(big.Int).SetUint64(receipt.GasUsed)
feeAmount := gasUsedBig.Mul(gasUsedBig, body.tx.GasPrice())

loadedTxs := body.LoadedTransaction()
loadedTxs.Transaction = body.tx
loadedTxs.FeeAmount = feeAmount
loadedTxs.Miner = MustChecksum(header.Coinbase.Hex())
loadedTxs.Receipt = receipt

if addTraces {
loadedTxs.Trace = traces
loadedTxs.RawTrace = rawTraces
}

tx, err := ec.populateTransaction(loadedTxs)
if err != nil {
return nil, fmt.Errorf("%w: cannot parse %s", err, loadedTxs.Transaction.Hash().Hex())
}
return tx, nil
}

// Block returns a populated block at the *RosettaTypes.PartialBlockIdentifier.
// If neither the hash or index is populated in the *RosettaTypes.PartialBlockIdentifier,
// the current block is returned.
Expand Down Expand Up @@ -237,7 +319,7 @@ func (ec *Client) Block(

// Header returns a block header from the current canonical chain. If number is
// nil, the latest known header is returned.
func (ec *Client) blockHeader(ctx context.Context, number *big.Int) (*types.Header, error) {
func (ec *Client) blockHeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
var head *types.Header
err := ec.c.CallContext(ctx, &head, "eth_getBlockByNumber", toBlockNumArg(number), false)
if err == nil && head == nil {
Expand All @@ -247,6 +329,21 @@ func (ec *Client) blockHeader(ctx context.Context, number *big.Int) (*types.Head
return head, err
}

// Header returns a block header from the current canonical chain. If hash is empty
// it returns error.
func (ec *Client) blockHeaderByHash(ctx context.Context, hash string) (*types.Header, error) {
var head *types.Header
if hash == "" {
return nil, errors.New("hash is empty")
}
err := ec.c.CallContext(ctx, &head, "eth_getBlockByHash", hash, false)
if err == nil && head == nil {
return nil, ethereum.NotFound
}

return head, err
}

type rpcBlock struct {
Hash common.Hash `json:"hash"`
Transactions []rpcTransaction `json:"transactions"`
Expand Down Expand Up @@ -414,6 +511,30 @@ func effectiveGasPrice(tx *EthTypes.Transaction, baseFee *big.Int) (*big.Int, er
return new(big.Int).Add(tip, baseFee), nil
}

func (ec *Client) getTransactionTraces(
ctx context.Context,
transactionHash common.Hash,
) (*Call, json.RawMessage, error) {
if err := ec.traceSemaphore.Acquire(ctx, semaphoreTraceWeight); err != nil {
return nil, nil, err
}
defer ec.traceSemaphore.Release(semaphoreTraceWeight)

var call *Call
var raw json.RawMessage
err := ec.c.CallContext(ctx, &raw, "debug_traceTransaction", transactionHash, ec.tc)
if err != nil {
return nil, nil, err
}

// Decode *Call
if err := json.Unmarshal(raw, &call); err != nil {
return nil, nil, err
}

return call, raw, nil
}

func (ec *Client) getBlockTraces(
ctx context.Context,
blockHash common.Hash,
Expand Down Expand Up @@ -1069,7 +1190,7 @@ func (ec *Client) populateTransactions(
func (ec *Client) populateTransaction(
tx *loadedTransaction,
) (*RosettaTypes.Transaction, error) {
ops := []*RosettaTypes.Operation{}
var ops []*RosettaTypes.Operation

// Compute fee operations
feeOps := feeOps(tx)
Expand Down
147 changes: 147 additions & 0 deletions ethereum/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,20 @@ func TestBlock_FirstBlock(t *testing.T) {
mockGraphQL.AssertExpectations(t)
}

func jsonifyTransaction(b *RosettaTypes.Transaction) (*RosettaTypes.Transaction, error) {
bytes, err := json.Marshal(b)
if err != nil {
return nil, err
}

var tx RosettaTypes.Transaction
if err := json.Unmarshal(bytes, &tx); err != nil {
return nil, err
}

return &tx, nil
}

func jsonifyBlock(b *RosettaTypes.Block) (*RosettaTypes.Block, error) {
bytes, err := json.Marshal(b)
if err != nil {
Expand All @@ -1461,6 +1475,139 @@ func jsonifyBlock(b *RosettaTypes.Block) (*RosettaTypes.Block, error) {
return &bo, nil
}

func TestTransaction_Hash(t *testing.T) {
mockJSONRPC := &mocks.JSONRPC{}
mockGraphQL := &mocks.GraphQL{}
txHash := "0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4"
blockHash := "0xc10a51a3898a85c7165a9d883acc9a68f139934d0cb91dfad4c7d3a7c1a1960d"

tc, err := testTraceConfig()
assert.NoError(t, err)
c := &Client{
c: mockJSONRPC,
g: mockGraphQL,
tc: tc,
p: params.RopstenChainConfig,
traceSemaphore: semaphore.NewWeighted(100),
}

ctx := context.Background()
mockJSONRPC.On(
"CallContext",
ctx,
mock.Anything,
"eth_getTransactionByHash",
txHash,
).Return(
nil,
).Run(
func(args mock.Arguments) {
r := args.Get(1).(*json.RawMessage)

file, err := ioutil.ReadFile(
"testdata/transaction_0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4.json",
) // nolint
assert.NoError(t, err)

*r = json.RawMessage(file)
},
).Once()

mockJSONRPC.On(
"CallContext",
ctx,
mock.Anything,
"eth_getBlockByHash",
blockHash,
false,
).Return(
nil,
).Run(
func(args mock.Arguments) {
r := args.Get(1).(**types.Header)

file, err := ioutil.ReadFile(
"testdata/block_0xc10a51a3898a85c7165a9d883acc9a68f139934d0cb91dfad4c7d3a7c1a1960d.json",
) // nolint
assert.NoError(t, err)

*r = new(types.Header)

assert.NoError(t, (*r).UnmarshalJSON(file))
},
).Once()

mockJSONRPC.On(
"CallContext",
ctx,
mock.Anything,
"eth_getTransactionReceipt",
common.HexToHash(txHash),
).Return(
nil,
).Run(
func(args mock.Arguments) {
r := args.Get(1).(**types.Receipt)

file, err := ioutil.ReadFile(
"testdata/tx_receipt_0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4.json",
) // nolint
assert.NoError(t, err)

*r = new(types.Receipt)

assert.NoError(t, (*r).UnmarshalJSON(file))
},
).Once()

mockJSONRPC.On(
"CallContext",
ctx,
mock.Anything,
"debug_traceTransaction",
common.HexToHash(txHash),
tc,
).Return(
nil,
).Run(
func(args mock.Arguments) {
r := args.Get(1).(*json.RawMessage)

file, err := ioutil.ReadFile(
"testdata/transaction_trace_0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4.json",
) // nolint
assert.NoError(t, err)

*r = json.RawMessage(file)
},
).Once()

correctRaw, err := ioutil.ReadFile(
"testdata/transaction_response_0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4.json",
) // nolint
assert.NoError(t, err)
var correct *RosettaTypes.BlockTransactionResponse
assert.NoError(t, json.Unmarshal(correctRaw, &correct))

resp, err := c.Transaction(
ctx,
&RosettaTypes.BlockIdentifier{
Hash: "0xc10a51a3898a85c7165a9d883acc9a68f139934d0cb91dfad4c7d3a7c1a1960d",
},
&RosettaTypes.TransactionIdentifier{
Hash: "0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4",
},
)
assert.NoError(t, err)

jsonResp, err := jsonifyTransaction(resp)
assert.NoError(t, err)
assert.Equal(t, correct.Transaction, jsonResp)

mockJSONRPC.AssertExpectations(t)
mockGraphQL.AssertExpectations(t)
}

// Block with transaction
func TestBlock_10994(t *testing.T) {
mockJSONRPC := &mocks.JSONRPC{}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"size": "0x7d8",
"stateRoot": "0x398dea392039f685330007d8bf0dfa19a11b4416dca25da81eda376273a2f314",
"timestamp": "0x5839bac0",
"transactions": [
"0xe4c2e3ed6213995ae2eae1159d249ec65d4878d567b93201ed11cd9c61111bad",
"0xe5f20e54a54fcd8ff72d65dafd6329504d8b4928b7907c00da9c3f2d27ff5946",
"0x026f61be527edd51fda90c20494fb2967a81042f425158121ad718a6e6769823",
"0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4",
"0xe85ce94f56959423940ef54977ba9270c882c67b513dba7a5d470e11402ef0f6",
"0x59bc342f33b4df5913356dd41f8b7f8642d14bd81a22c6cdc97b0c14ad77e048"
],
"difficulty": "0x5e4e9b1",
"logsBloom": "0x04002000000000000000000000000000000500000000000000000020000000000000000010000000000000000000000000000000000000000000100000000000000000000000000000000000004000000000000000000400000008000000000000000000020440000000000000000800400000000004000000000000000000000000008800000000000000000000000000000000000000000000000000080088000000000000000000000100000000000000000000000000000000000400000010000000100000200040000040000000000000000000000000000000400020000000000000000000000000000000000008000000008000000000000000004000",
"totalDifficulty": "0x3bd0bc576e3",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"parentHash": "0xd027a9008c5cb9da99ce459159a187a15e476fbb20a420abbd0964ca5720cd8e",
"hash": "0xc10a51a3898a85c7165a9d883acc9a68f139934d0cb91dfad4c7d3a7c1a1960d",
"extraData": "0xd583010503846765746885676f312e37856c696e7578",
"gasUsed": "0xcda60",
"transactionsRoot": "0x0ab42b6a48a9eea1bd4611a39342a9eab0ac3fefb636c53b6de61eda0ac3b852",
"gasLimit": "0x47e7c4",
"number": "0xafc8",
"miner": "0xffc614ee978630d7fb0c06758deb580c152154d3",
"uncles": [],
"nonce": "0x59dfb51a4ab60a79",
"mixHash": "0x004f27be284f8536342d6e00ab9b1a12e03c40df3dfc4699ff55798dbd67f945",
"receiptsRoot": "0x94fe13083791d63d3f1318ba6d73407cf059fe344cbad5d6d4abd9f00b80edfd"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hash":"0x9cc8e6a09ae9cbdb7da77515110a8e343a945df4269c53842dd26969d32c6cc4",
"blockHash":"0xc10a51a3898a85c7165a9d883acc9a68f139934d0cb91dfad4c7d3a7c1a1960d",
"blockNumber":"0xafc8",
"from":"0x687422eea2cb73b5d3e242ba5456b782919afc85",
"gas":"0x4cb26",
"gasPrice":"0x4a817c800",
"input":"0x",
"nonce":"0x9c6",
"r":"0x3dbbbf3bac12e7e5caa704a77d35a3dd7bf0374475f0970e6c3d0033d54436c0",
"s":"0x31b023f55d877ee6888583f86dc019c283b3d7255314bcd753346475baceb964",
"to":"0xc662a694fdaa5406a8ee2ca2e94890d58ab578d9",
"transactionIndex":"0x3",
"v":"0x1c",
"value":"0xde0b6b3a7640000"
}
Loading