Skip to content

Commit 6c7c0ad

Browse files
authored
Introducing log, tx decoder including fixes for constructor (#163)
1 parent a87d757 commit 6c7c0ad

File tree

7 files changed

+355
-27
lines changed

7 files changed

+355
-27
lines changed

bytecode/constructor.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ type Argument struct {
2121
// Constructor represents a contract constructor.
2222
// It includes the ABI of the constructor, the raw signature, and the arguments.
2323
type Constructor struct {
24-
Abi string `json:"abi"` // ABI of the constructor
25-
Parsed abi.ABI `json:"-"` // Parsed ABI of the constructor
26-
SignatureRaw string `json:"signature_raw"` // Raw signature of the constructor
27-
Arguments []Argument // List of arguments in the constructor
24+
Abi string `json:"abi"` // ABI of the constructor
25+
Parsed abi.ABI `json:"-"` // Parsed ABI of the constructor
26+
SignatureRaw string `json:"signature_raw"` // Raw signature of the constructor
27+
Arguments []Argument `json:"arguments"` // List of arguments in the constructor
2828
UnpackedArguments []interface{} `json:"unpacked_arguments"` // List of unpacked arguments in the constructor
2929
}
3030

@@ -43,11 +43,6 @@ func (c *Constructor) Pack() ([]byte, error) {
4343
// If they match, it creates an Argument object for each input and adds it to the arguments slice.
4444
// Finally, it returns a Constructor object containing the ABI, raw signature, and arguments.
4545
func DecodeConstructorFromAbi(bytecode []byte, constructorAbi string) (*Constructor, error) {
46-
/* var fh interface{}
47-
json.Unmarshal([]byte(constructorAbi), &fh)
48-
f, _ := utils.ToJSONPretty(fh)
49-
fmt.Println(string(f)) */
50-
5146
parsed, err := abi.JSON(strings.NewReader(constructorAbi))
5247
if err != nil {
5348
return nil, fmt.Errorf("failed to parse ABI: %w", err)

bytecode/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Package bytecode provides tools for decoding and analyzing Ethereum contract, transaction, event and log bytecode.
2+
Package bytecode provides tools for decoding and analyzing Ethereum contract, transaction, events, and log bytecode.
33
The package is designed to extract and interpret metadata from Ethereum contract creation bytecode. It provides a Metadata struct that represents the metadata contained in the bytecode, as defined by the Solidity compiler. This includes information such as the IPFS hash of the metadata, the Swarm hash of the metadata, experimental metadata, and the version of the Solidity compiler used.
44
*/
55
package bytecode

bytecode/log.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package bytecode
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"math/big"
7+
"strings"
8+
9+
abi "github.com/ethereum/go-ethereum/accounts/abi"
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/unpackdev/solgo/utils"
13+
)
14+
15+
// Topic represents a single decoded topic from an Ethereum event log. Topics are attributes
16+
// of an event, such as the method signature and indexed parameters.
17+
type Topic struct {
18+
Name string `json:"name"` // The name of the topic.
19+
Value any `json:"value"` // The value of the topic, decoded into the appropriate Go data type.
20+
}
21+
22+
// Log encapsulates a decoded Ethereum event log. It includes the event's details such as its name,
23+
// signature, the contract that emitted the event, and the decoded data and topics.
24+
type Log struct {
25+
Event *abi.Event `json:"-"` // ABI definition of the log's event.
26+
Address common.Address `json:"address"` // Address of the contract that emitted the event.
27+
Abi string `json:"abi"` // ABI string of the event.
28+
SignatureHex common.Hash `json:"signature_hex"` // Hex-encoded signature of the event.
29+
Signature string `json:"signature"` // Signature of the event.
30+
Type utils.LogEventType `json:"type"` // Type of the event, classified by solgo.
31+
Name string `json:"name"` // Name of the event.
32+
Data map[string]any `json:"data"` // Decoded event data.
33+
Topics []Topic `json:"topics"` // Decoded topics of the event.
34+
}
35+
36+
// DecodeLogFromAbi decodes an Ethereum event log using the provided ABI data. It returns a Log
37+
// instance containing the decoded event name, data, and topics. The function requires the event log
38+
// and its ABI as inputs. It handles errors such as missing topics or failure to parse the ABI.
39+
func DecodeLogFromAbi(log *types.Log, abiData []byte) (*Log, error) {
40+
if log == nil || len(log.Topics) < 1 {
41+
return nil, fmt.Errorf("log is nil or has no topics")
42+
}
43+
44+
logABI, err := abi.JSON(bytes.NewReader(abiData))
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to parse abi: %s", err)
47+
}
48+
49+
event, err := logABI.EventByID(log.Topics[0])
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to get event by id %s: %s", log.Topics[0].Hex(), err)
52+
}
53+
54+
data := make(map[string]any)
55+
if err := event.Inputs.UnpackIntoMap(data, log.Data); err != nil {
56+
return nil, fmt.Errorf("failed to unpack inputs into map: %s", err)
57+
}
58+
59+
decodedTopics := make([]Topic, len(log.Topics))
60+
for i, topic := range log.Topics {
61+
if i == 0 {
62+
continue
63+
}
64+
65+
decodedTopic, err := decodeTopic(topic, event.Inputs[i-1])
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to decode topic: %s", err)
68+
}
69+
70+
decodedTopics[i] = Topic{
71+
Name: event.Inputs[i-1].Name,
72+
Value: decodedTopic,
73+
}
74+
}
75+
76+
eventAbi, err := utils.EventToABI(event)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to cast event into the abi: %w", err)
79+
}
80+
81+
toReturn := &Log{
82+
Event: event,
83+
Address: log.Address,
84+
Abi: eventAbi,
85+
SignatureHex: log.Topics[0],
86+
Signature: strings.TrimLeft(event.String(), "event "),
87+
Name: event.Name,
88+
Type: utils.LogEventType(strings.ToLower(event.Name)),
89+
Data: data,
90+
Topics: decodedTopics[1:], // Exclude the first topic (event signature)
91+
}
92+
93+
return toReturn, nil
94+
}
95+
96+
// decodeTopic decodes a single topic from an Ethereum event log based on its ABI argument type.
97+
// It supports various data types including addresses, booleans, integers, strings, bytes, and more.
98+
// This function is internal and used within DecodeLogFromAbi to process each topic individually.
99+
func decodeTopic(topic common.Hash, argument abi.Argument) (interface{}, error) {
100+
switch argument.Type.T {
101+
case abi.AddressTy:
102+
return common.BytesToAddress(topic.Bytes()), nil
103+
case abi.BoolTy:
104+
return topic[common.HashLength-1] == 1, nil
105+
case abi.IntTy, abi.UintTy:
106+
size := argument.Type.Size
107+
if size > 256 {
108+
return nil, fmt.Errorf("unsupported integer size: %d", size)
109+
}
110+
integer := new(big.Int).SetBytes(topic[:])
111+
if argument.Type.T == abi.IntTy && size < 256 {
112+
integer = adjustIntSize(integer, size)
113+
}
114+
return integer, nil
115+
case abi.StringTy:
116+
return topic, nil
117+
case abi.BytesTy, abi.FixedBytesTy:
118+
return topic.Bytes(), nil
119+
case abi.SliceTy, abi.ArrayTy:
120+
return nil, fmt.Errorf("array/slice decoding not implemented")
121+
default:
122+
return nil, fmt.Errorf("decoding for type %v not implemented", argument.Type.T)
123+
}
124+
}
125+
126+
// adjustIntSize adjusts the size of an integer to match its ABI-specified size, which is relevant
127+
// for signed integers smaller than 256 bits. This function ensures the integer is correctly
128+
// interpreted according to its defined bit size in the ABI.
129+
func adjustIntSize(integer *big.Int, size int) *big.Int {
130+
if size == 256 || integer.Bit(size-1) == 0 {
131+
return integer
132+
}
133+
return new(big.Int).Sub(integer, new(big.Int).Lsh(big.NewInt(1), uint(size)))
134+
}
135+
136+
// GetTopicByName searches for and returns a Topic by its name from a slice of Topic instances.
137+
// It facilitates accessing specific topics directly by name rather than iterating over the slice.
138+
// If the topic is not found, it returns nil.
139+
func GetTopicByName(name string, topics []Topic) *Topic {
140+
for _, topic := range topics {
141+
if topic.Name == name {
142+
return &topic
143+
}
144+
}
145+
return nil
146+
}

bytecode/metadata.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ import (
1616
// The structure and encoding of the metadata is defined by the Solidity compiler.
1717
// More information can be found at https://docs.soliditylang.org/en/v0.8.20/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
1818
type Metadata struct {
19-
executionBytecode []byte // The execution bytecode of the contract
20-
cborLength int16 // The length of the CBOR metadata
21-
auxbytes []byte // The raw CBOR metadata
22-
Ipfs []byte `cbor:"ipfs"` // The IPFS hash of the metadata, if present
23-
Bzzr1 []byte `cbor:"bzzr1"` // The Swarm hash of the metadata, if present (version 1)
24-
Bzzr0 []byte `cbor:"bzzr0"` // The Swarm hash of the metadata, if present (version 0)
25-
Experimental []byte `cbor:"experimental"` // Experimental metadata, if present
26-
Solc []byte `cbor:"solc"` // The version of the Solidity compiler used
19+
executionBytecode []byte // The execution bytecode of the contract
20+
cborLength int16 // The length of the CBOR metadata
21+
auxbytes []byte // The raw CBOR metadata
22+
Ipfs []byte `cbor:"ipfs"` // The IPFS hash of the metadata, if present
23+
Bzzr1 []byte `cbor:"bzzr1"` // The Swarm hash of the metadata, if present (version 1)
24+
Bzzr0 []byte `cbor:"bzzr0"` // The Swarm hash of the metadata, if present (version 0)
25+
Experimental interface{} `cbor:"experimental"` // Experimental metadata, if present
26+
Solc []byte `cbor:"solc"` // The version of the Solidity compiler used
2727
}
2828

29+
// ToProto converts the Metadata instance into a protobuf representation, suitable for
30+
// serialization and transmission across different systems or networks.
2931
func (m *Metadata) ToProto() *metadata_pb.BytecodeMetadata {
3032
return &metadata_pb.BytecodeMetadata{
3133
ExecutionBytecode: m.executionBytecode,
@@ -36,7 +38,7 @@ func (m *Metadata) ToProto() *metadata_pb.BytecodeMetadata {
3638
Bzzr0: m.GetBzzr0(),
3739
Experimental: m.GetExperimental(),
3840
Solc: m.GetCompilerVersion(),
39-
Solgo: "",
41+
Solgo: "", // TODO: Solgo version should be appended...
4042
}
4143
}
4244

@@ -51,11 +53,19 @@ func (m *Metadata) GetCompilerVersion() string {
5153

5254
// GetExperimental returns whether the contract includes experimental metadata.
5355
func (m *Metadata) GetExperimental() bool {
54-
toReturn, err := strconv.ParseBool(string(m.Experimental))
55-
if err != nil {
56-
return false
56+
if experimental, ok := m.Experimental.(string); ok {
57+
toReturn, err := strconv.ParseBool(experimental)
58+
if err != nil {
59+
return false
60+
}
61+
return toReturn
62+
}
63+
64+
if experimental, ok := m.Experimental.(bool); ok {
65+
return experimental
5766
}
58-
return toReturn
67+
68+
return false
5969
}
6070

6171
// GetIPFS returns the IPFS hash of the contract's metadata, if present.
@@ -134,7 +144,6 @@ func DecodeContractMetadata(bytecode []byte) (*Metadata, error) {
134144
if len(bytecode) >= bytesLength+cborLength {
135145
toReturn.executionBytecode = bytecode[:len(bytecode)-bytesLength-cborLength]
136146
toReturn.auxbytes = bytecode[len(bytecode)-bytesLength-cborLength : len(bytecode)-bytesLength]
137-
138147
if err := cbor.Unmarshal(toReturn.auxbytes, &toReturn); err != nil {
139148
return nil, err
140149
}

bytecode/metadata_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ func TestDecodeContractCreationMetadata(t *testing.T) {
6767
assert.NotNil(t, got.GetExecutionBytecode())
6868
assert.NotNil(t, got.ToProto())
6969
assert.GreaterOrEqual(t, len(got.GetUrls()), 0)
70-
71-
got.Experimental = []byte("true")
72-
assert.True(t, got.GetExperimental())
7370
}
7471
})
7572
}

bytecode/transaction.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package bytecode
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"strings"
7+
8+
abi "github.com/ethereum/go-ethereum/accounts/abi"
9+
"github.com/unpackdev/solgo/utils"
10+
)
11+
12+
// Transaction encapsulates a decoded Ethereum transaction, providing detailed information
13+
// about the transaction's method, its arguments, and the associated ABI.
14+
// This structured format makes it easier to work with Ethereum transactions programmatically.
15+
type Transaction struct {
16+
Abi string `json:"abi"` // ABI string of the transaction's method.
17+
SignatureBytes []byte `json:"signature_bytes"` // Raw signature bytes of the transaction.
18+
Signature string `json:"signature"` // Human-readable signature of the transaction's method.
19+
Type utils.TransactionMethodType `json:"type"` // Type of the transaction, classified by its method name.
20+
Name string `json:"name"` // Name of the transaction's method.
21+
Method *abi.Method `json:"-"` // ABI method information, not serialized to JSON.
22+
Inputs map[string]interface{} `json:"inputs"` // Decoded arguments passed to the transaction's method.
23+
}
24+
25+
// DecodeTransactionFromAbi decodes an Ethereum transaction using the provided ABI.
26+
// It extracts the method signature and arguments from the raw transaction data, constructing
27+
// a Transaction object that includes this information along with the method's ABI.
28+
//
29+
// The function requires the raw transaction data (`data`) and the ABI of the smart contract
30+
// (`abiData`) in JSON format. It returns a pointer to a Transaction object, populated with
31+
// the decoded method information and its arguments, or an error if decoding fails.
32+
//
33+
// This function simplifies the process of interacting with raw Ethereum transactions, making
34+
// it easier to analyze and use the transaction data programmatically.
35+
func DecodeTransactionFromAbi(data []byte, abiData []byte) (*Transaction, error) {
36+
// The first 4 bytes of the data represent the ID of the method in the ABI.
37+
methodSigData := data[:4]
38+
39+
contractABI, err := abi.JSON(bytes.NewReader(abiData))
40+
if err != nil {
41+
return nil, fmt.Errorf("failed to parse abi: %s", err)
42+
}
43+
44+
method, err := contractABI.MethodById(methodSigData)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to get method by id: %s", err)
47+
}
48+
49+
inputsSigData := data[4:]
50+
inputsMap := make(map[string]interface{})
51+
if err := method.Inputs.UnpackIntoMap(inputsMap, inputsSigData); err != nil {
52+
return nil, fmt.Errorf("failed to unpack inputs into map: %s", err)
53+
}
54+
55+
txAbi, err := utils.MethodToABI(method)
56+
if err != nil {
57+
return nil, fmt.Errorf("failure to cast method into abi: %w", err)
58+
}
59+
60+
return &Transaction{
61+
Abi: txAbi,
62+
SignatureBytes: methodSigData,
63+
Signature: method.String(),
64+
Name: method.Name,
65+
Method: method,
66+
Type: utils.TransactionMethodType(strings.ToLower(method.Name)),
67+
Inputs: inputsMap,
68+
}, nil
69+
}

0 commit comments

Comments
 (0)