Skip to content

Commit 7d7de32

Browse files
Add options for output formatting to JSON/RPC object serialization
Signed-off-by: Peter Broadhurst <peter.broadhurst@kaleido.io>
1 parent 5826aff commit 7d7de32

File tree

5 files changed

+720
-9
lines changed

5 files changed

+720
-9
lines changed

internal/msgs/en_error_messages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,6 @@ var (
8282
MsgInMemoryPartialChainNotCaughtUp = ffe("FF23062", "In-memory partial chain is waiting for the transaction block %d (%s) to be indexed")
8383
MsgFailedToBuildExistingConfirmationInvalid = ffe("FF23063", "Failed to build confirmations, existing confirmations are not valid")
8484
MsgFromBlockInvalid = ffe("FF23064", "From block invalid. Must be 'earliest', 'latest' or a decimal: %s", http.StatusBadRequest)
85+
MsgInvalidJSONFormatOptions = ffe("FF23065", "The JSON formatting options must be a valid set of key=value pairs in URL query string format '%s'")
86+
MsgUnknownJSONFormatOptions = ffe("FF23066", "JSON formatting option unknown %s=%s")
8587
)

pkg/ethrpc/ethrpc.go

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,62 @@
1616

1717
package ethrpc
1818

19-
import "github.com/hyperledger/firefly-signer/pkg/ethtypes"
19+
import (
20+
"context"
21+
"encoding/json"
22+
"math/big"
23+
24+
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
25+
)
2026

2127
// TxReceiptJSONRPC is the receipt obtained over JSON/RPC from the ethereum client, with gas used, logs and contract address
2228
type TxReceiptJSONRPC struct {
29+
TransactionHash ethtypes.HexBytes0xPrefix `json:"transactionHash"`
30+
TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"`
2331
BlockHash ethtypes.HexBytes0xPrefix `json:"blockHash"`
2432
BlockNumber *ethtypes.HexInteger `json:"blockNumber"`
25-
ContractAddress *ethtypes.Address0xHex `json:"contractAddress"`
26-
CumulativeGasUsed *ethtypes.HexInteger `json:"cumulativeGasUsed"`
2733
From *ethtypes.Address0xHex `json:"from"`
34+
To *ethtypes.Address0xHex `json:"to"`
35+
CumulativeGasUsed *ethtypes.HexInteger `json:"cumulativeGasUsed"`
36+
EffectiveGasPrice *ethtypes.HexInteger `json:"effectiveGasPrice"`
2837
GasUsed *ethtypes.HexInteger `json:"gasUsed"`
38+
ContractAddress *ethtypes.Address0xHex `json:"contractAddress"`
2939
Logs []*LogJSONRPC `json:"logs"`
40+
LogsBloom ethtypes.HexBytes0xPrefix `json:"logsBloom"`
41+
Type *ethtypes.HexInteger `json:"type"`
3042
Status *ethtypes.HexInteger `json:"status"`
31-
To *ethtypes.Address0xHex `json:"to"`
32-
TransactionHash ethtypes.HexBytes0xPrefix `json:"transactionHash"`
33-
TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"`
3443
RevertReason *ethtypes.HexBytes0xPrefix `json:"revertReason"`
3544
}
3645

46+
func (txr *TxReceiptJSONRPC) MarshalFormat(ctx context.Context, format JSONFormatOptions) (_ []byte, err error) {
47+
logsArray := make([]json.RawMessage, len(txr.Logs))
48+
for i, l := range txr.Logs {
49+
if logsArray[i], err = l.MarshalFormat(ctx, format); err != nil {
50+
return nil, err
51+
}
52+
}
53+
formatMap := map[string]any{
54+
"transactionHash": ([]byte)(txr.TransactionHash),
55+
"transactionIndex": (*big.Int)(txr.TransactionIndex),
56+
"blockHash": ([]byte)(txr.BlockHash),
57+
"blockNumber": (*big.Int)(txr.BlockNumber),
58+
"from": (*[20]byte)(txr.From),
59+
"to": (*[20]byte)(txr.To),
60+
"cumulativeGasUsed": (*big.Int)(txr.CumulativeGasUsed),
61+
"effectiveGasPrice": (*big.Int)(txr.EffectiveGasPrice),
62+
"gasUsed": (*big.Int)(txr.GasUsed),
63+
"contractAddress": (*[20]byte)(txr.ContractAddress),
64+
"logs": logsArray,
65+
"logsBloom": ([]byte)(txr.LogsBloom),
66+
"status": (*big.Int)(txr.Status),
67+
"type": (*big.Int)(txr.Type),
68+
}
69+
if txr.RevertReason != nil {
70+
formatMap["revertReason"] = ([]byte)(*txr.RevertReason)
71+
}
72+
return format.MarshalFormattedMap(ctx, formatMap)
73+
}
74+
3775
// TxInfoJSONRPC is the transaction info obtained over JSON/RPC from the ethereum client, with input data
3876
type TxInfoJSONRPC struct {
3977
BlockHash ethtypes.HexBytes0xPrefix `json:"blockHash"` // null if pending
@@ -43,12 +81,32 @@ type TxInfoJSONRPC struct {
4381
GasPrice *ethtypes.HexInteger `json:"gasPrice"`
4482
Hash ethtypes.HexBytes0xPrefix `json:"hash"`
4583
Input ethtypes.HexBytes0xPrefix `json:"input"`
46-
R *ethtypes.HexInteger `json:"r"`
47-
S *ethtypes.HexInteger `json:"s"`
84+
Nonce *ethtypes.HexInteger `json:"nonce"`
4885
To *ethtypes.Address0xHex `json:"to"`
4986
TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"` // null if pending
50-
V *ethtypes.HexInteger `json:"v"`
5187
Value *ethtypes.HexInteger `json:"value"`
88+
V *ethtypes.HexInteger `json:"v"`
89+
R *ethtypes.HexInteger `json:"r"`
90+
S *ethtypes.HexInteger `json:"s"`
91+
}
92+
93+
func (txi *TxInfoJSONRPC) MarshalFormat(ctx context.Context, format JSONFormatOptions) (_ []byte, err error) {
94+
return format.MarshalFormattedMap(ctx, map[string]any{
95+
"blockHash": ([]byte)(txi.BlockHash),
96+
"blockNumber": (*big.Int)(txi.BlockNumber),
97+
"from": (*[20]byte)(txi.From),
98+
"gas": (*big.Int)(txi.Gas),
99+
"gasPrice": (*big.Int)(txi.GasPrice),
100+
"hash": ([]byte)(txi.Hash),
101+
"input": ([]byte)(txi.Input),
102+
"nonce": (*big.Int)(txi.Nonce),
103+
"to": (*[20]byte)(txi.To),
104+
"transactionIndex": (*big.Int)(txi.TransactionIndex),
105+
"value": (*big.Int)(txi.Value),
106+
"v": (*big.Int)(txi.V),
107+
"r": (*big.Int)(txi.R),
108+
"s": (*big.Int)(txi.S),
109+
})
52110
}
53111

54112
type LogFilterJSONRPC struct {
@@ -70,6 +128,24 @@ type LogJSONRPC struct {
70128
Topics []ethtypes.HexBytes0xPrefix `json:"topics"`
71129
}
72130

131+
func (l *LogJSONRPC) MarshalFormat(ctx context.Context, format JSONFormatOptions) (_ []byte, err error) {
132+
topicsArray := make([]any, len(l.Topics))
133+
for i, t := range l.Topics {
134+
topicsArray[i] = ([]byte)(t)
135+
}
136+
return format.MarshalFormattedMap(ctx, map[string]any{
137+
"removed": l.Removed,
138+
"logIndex": (*big.Int)(l.LogIndex),
139+
"transactionIndex": (*big.Int)(l.TransactionIndex),
140+
"blockNumber": (*big.Int)(l.BlockNumber),
141+
"transactionHash": ([]byte)(l.TransactionHash),
142+
"blockHash": ([]byte)(l.BlockHash),
143+
"address": (*[20]byte)(l.Address),
144+
"data": ([]byte)(l.Data),
145+
"topics": topicsArray,
146+
})
147+
}
148+
73149
// BlockInfoJSONRPC are the info fields we parse from the JSON/RPC response, and cache
74150
type BlockInfoJSONRPC struct {
75151
Number *ethtypes.HexInteger `json:"number"`
@@ -79,3 +155,18 @@ type BlockInfoJSONRPC struct {
79155
LogsBloom ethtypes.HexBytes0xPrefix `json:"logsBloom"`
80156
Transactions []ethtypes.HexBytes0xPrefix `json:"transactions"`
81157
}
158+
159+
func (bi *BlockInfoJSONRPC) MarshalFormat(ctx context.Context, format JSONFormatOptions) (_ []byte, err error) {
160+
txnArray := make([]any, len(bi.Transactions))
161+
for i, t := range bi.Transactions {
162+
txnArray[i] = ([]byte)(t)
163+
}
164+
return format.MarshalFormattedMap(ctx, map[string]any{
165+
"number": (*big.Int)(bi.Number),
166+
"hash": ([]byte)(bi.Hash),
167+
"parentHash": ([]byte)(bi.ParentHash),
168+
"timestamp": (*big.Int)(bi.Timestamp),
169+
"logsBloom": ([]byte)(bi.LogsBloom),
170+
"transactions": txnArray,
171+
})
172+
}

pkg/ethrpc/ethrpc_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright © 2026 Kaleido, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package ethrpc
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"math/big"
24+
"testing"
25+
26+
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
const sampleBlock = `{
31+
"number": "0xe5f2",
32+
"hash": "0xd33367228e0a0e3667c910c7d92d3f6e724e2b6e2f671b28823a22f82597d023",
33+
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
34+
"parentHash": "0x78ec31452f053f75665033d4957b8b33283c55e1c7239dc5facbb684a866492e",
35+
"nonce": "0x0000000000000000",
36+
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
37+
"logsBloom": "0x00000000000000000000000000000000000000000000000000000080000100002000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000010008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020200000000000000204000000000000000000000000000002000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000404000000000000000",
38+
"transactionsRoot": "0x77c428651debcc42d181d9067386d807b204235ebb3e45dfa5f1e6e71c0b67de",
39+
"stateRoot": "0xaebe1ab7b242b261b1aced1a1ed729c3d6436634ff656efecad4609b38ee63a3",
40+
"receiptsRoot": "0x0946e4baf57a46888aa1a78dc974a01a56bb221ae8d6ee381942695596f976ec",
41+
"miner": "0xef5880ec6b859b6949d88ddbd6ec18e96d0f14aa",
42+
"difficulty": "0x1",
43+
"totalDifficulty": "0xe5f3",
44+
"extraData": "0xf87ea00000000000000000000000000000000000000000000000000000000000000000d594ef5880ec6b859b6949d88ddbd6ec18e96d0f14aac080f843b8410469b43f8d873e7b3c54c0d2a606a37af533994653f077b234a563f00723641a7ad56e6ba07d56fb38ec68bc9b6234d54fe62730f41118658a12c886fb783c3100",
45+
"baseFeePerGas": "0x0",
46+
"size": "0x3dc",
47+
"gasLimit": "0x2fefd800",
48+
"gasUsed": "0x197b8",
49+
"timestamp": "0x6849f937",
50+
"uncles": [],
51+
"transactions": ["0x6431a7fc56e24319bb431ed3040d77d1a7b54add9207266c19df6fc53961da99", "0xa4dd8fc1be327a13c8f5be7b74331351c419fa8b908ff7277786270ebdf2a875"]
52+
}`
53+
54+
const sampleReceipt = `{
55+
"blockHash": "0xd33367228e0a0e3667c910c7d92d3f6e724e2b6e2f671b28823a22f82597d023",
56+
"blockNumber": "0xe5f2",
57+
"contractAddress": null,
58+
"cumulativeGasUsed": "0xcbe8",
59+
"from": "0x03a85df677b2aa0f7cccc942242ee900de505ce8",
60+
"gasUsed": "0xcbe8",
61+
"effectiveGasPrice": "0x0",
62+
"logs": [
63+
{
64+
"address": "0xaa75b5001274491c0985ba1012b09dfc02d9675d",
65+
"topics": [
66+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
67+
"0x00000000000000000000000003a85df677b2aa0f7cccc942242ee900de505ce8",
68+
"0x000000000000000000000000af5ce0b6c5745e49b4292794496bf2a08b97608b"
69+
],
70+
"data": "0x00000000000000000000000000000000000000000000000246ddf97976680000",
71+
"blockNumber": "0xe5f2",
72+
"transactionHash": "0x6431a7fc56e24319bb431ed3040d77d1a7b54add9207266c19df6fc53961da99",
73+
"transactionIndex": "0x0",
74+
"blockHash": "0xd33367228e0a0e3667c910c7d92d3f6e724e2b6e2f671b28823a22f82597d023",
75+
"logIndex": "0x0",
76+
"removed": false
77+
}
78+
],
79+
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020200000000000000204000000000000000000000000000002000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000404000000000000000",
80+
"status": "0x1",
81+
"to": "0xaa75b5001274491c0985ba1012b09dfc02d9675d",
82+
"transactionHash": "0x6431a7fc56e24319bb431ed3040d77d1a7b54add9207266c19df6fc53961da99",
83+
"transactionIndex": "0x0",
84+
"type": "0x0"
85+
}`
86+
87+
func TestFormatReceipt(t *testing.T) {
88+
var receipt TxReceiptJSONRPC
89+
err := json.Unmarshal([]byte(sampleReceipt), &receipt)
90+
require.NoError(t, err)
91+
92+
var jfo JSONFormatOptions = "number=hex&pretty=true"
93+
94+
ethSerialized, err := receipt.MarshalFormat(context.Background(), jfo)
95+
fmt.Println((string)(ethSerialized))
96+
require.NoError(t, err)
97+
require.JSONEq(t, sampleReceipt, string(ethSerialized))
98+
}
99+
100+
func TestFormatReceiptRevertReasonAndFormatVariation(t *testing.T) {
101+
revertReason := ethtypes.MustNewHexBytes0xPrefix("0xfeedbeef")
102+
largeInt, _ := new(big.Int).SetString("12300000000000000000000", 10)
103+
receipt := &TxReceiptJSONRPC{
104+
BlockHash: ethtypes.MustNewHexBytes0xPrefix("0x3ef1ef8a761284b782eb1e7db3e42bbba3fe2626e5faaadb8ae94dfda8d2f4ca"),
105+
BlockNumber: ethtypes.NewHexInteger(largeInt),
106+
ContractAddress: ethtypes.MustNewAddress("0x4a77dbf4e2ebec9d7dbb6e44fec7b5857128969c"),
107+
RevertReason: &revertReason,
108+
}
109+
110+
var jfo JSONFormatOptions = "number=json-number&bytes=base64&address=checksum&pretty=true"
111+
112+
ethSerialized, err := receipt.MarshalFormat(context.Background(), jfo)
113+
fmt.Println((string)(ethSerialized))
114+
require.NoError(t, err)
115+
require.JSONEq(t, `{
116+
"blockHash": "PvHvinYShLeC6x59s+Qru6P+Jibl+qrbiulN/ajS9Mo=",
117+
"blockNumber": 12300000000000000000000,
118+
"contractAddress": "0x4a77dBf4e2eBeC9d7dbB6E44FeC7B5857128969C",
119+
"cumulativeGasUsed": null,
120+
"effectiveGasPrice": null,
121+
"from": null,
122+
"gasUsed": null,
123+
"logs": [],
124+
"logsBloom": null,
125+
"revertReason": "/u2+7w==",
126+
"status": null,
127+
"to": null,
128+
"transactionHash": null,
129+
"transactionIndex": null,
130+
"type": null
131+
}`, string(ethSerialized))
132+
133+
}

0 commit comments

Comments
 (0)