Skip to content

Commit 066778f

Browse files
authored
Merge pull request #3 from bugout-dev/annotations
Optionally generate annotations for each interface
2 parents d7ada9c + bf7708c commit 066778f

File tree

9 files changed

+606
-15
lines changed

9 files changed

+606
-15
lines changed

abi.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package main
22

33
import (
44
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/ethereum/go-ethereum/crypto"
59
)
610

711
/**
@@ -53,6 +57,11 @@ type DecodedABI struct {
5357
Errors []ErrorItem
5458
}
5559

60+
type Annotations struct {
61+
InterfaceID []byte
62+
FunctionSelectors [][]byte
63+
}
64+
5665
func Decode(rawJSON []byte) (DecodedABI, error) {
5766
var typeDeclarations []TypeDeclaration
5867
var rawMessages []json.RawMessage
@@ -120,6 +129,33 @@ func Decode(rawJSON []byte) (DecodedABI, error) {
120129
return decodedABI, nil
121130
}
122131

132+
func MethodSelector(function FunctionItem) []byte {
133+
argumentTypes := make([]string, len(function.Inputs))
134+
for i, input := range function.Inputs {
135+
argumentTypes[i] = input.Type
136+
}
137+
argumentTypesString := strings.Join(argumentTypes, ",")
138+
signature := fmt.Sprintf("%s(%s)", function.Name, argumentTypesString)
139+
return crypto.Keccak256([]byte(signature))[:4]
140+
}
141+
142+
func Annotate(decodedABI DecodedABI) (Annotations, error) {
143+
var annotations Annotations
144+
annotations.InterfaceID = []byte{0x0, 0x0, 0x0, 0x0}
145+
annotations.FunctionSelectors = make([][]byte, len(decodedABI.Functions))
146+
for i, functionItem := range decodedABI.Functions {
147+
selector := MethodSelector(functionItem)
148+
annotations.FunctionSelectors[i] = selector
149+
150+
// XOR into InterfaceID byte by byte
151+
annotations.InterfaceID[0] ^= selector[0]
152+
annotations.InterfaceID[1] ^= selector[1]
153+
annotations.InterfaceID[2] ^= selector[2]
154+
annotations.InterfaceID[3] ^= selector[3]
155+
}
156+
return annotations, nil
157+
}
158+
123159
func (v Value) IsCompoundType() bool {
124160
return len(v.Components) > 0
125161
}

abi_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,44 @@
11
package main
22

33
import (
4+
"encoding/hex"
45
"os"
56
"testing"
67
)
78

9+
func TestMethodSelectorOnERC721SafeTransferFromWithoutCalldata(t *testing.T) {
10+
functionItem := FunctionItem{Type: "function", Name: "safeTransferFrom", Inputs: []Value{
11+
{Name: "from", Type: "address"},
12+
{Name: "to", Type: "address"},
13+
{Name: "tokenId", Type: "uint256"},
14+
}}
15+
16+
selector := MethodSelector(functionItem)
17+
18+
expectedSelectorString := "42842e0e"
19+
selectorString := hex.EncodeToString(selector)
20+
if selectorString != expectedSelectorString {
21+
t.Fatalf("Incorrect method selector for safeTransferFrom(address,address,uint256). Expected: %s, actual: %s", expectedSelectorString, selectorString)
22+
}
23+
}
24+
25+
func TestMethodSelectorOnERC721SafeTransferFromWithCalldata(t *testing.T) {
26+
functionItem := FunctionItem{Type: "function", Name: "safeTransferFrom", Inputs: []Value{
27+
{Name: "from", Type: "address"},
28+
{Name: "to", Type: "address"},
29+
{Name: "tokenId", Type: "uint256"},
30+
{Name: "data", Type: "bytes"},
31+
}}
32+
33+
selector := MethodSelector(functionItem)
34+
35+
expectedSelectorString := "b88d4fde"
36+
selectorString := hex.EncodeToString(selector)
37+
if selectorString != expectedSelectorString {
38+
t.Fatalf("Incorrect method selector for safeTransferFrom(address,address,uint256,bytes). Expected: %s, actual: %s", expectedSelectorString, selectorString)
39+
}
40+
}
41+
842
func TestDecodeOwnableERC20(t *testing.T) {
943
contents, readErr := os.ReadFile("fixtures/abis/OwnableERC20.json")
1044
if readErr != nil {
@@ -214,3 +248,26 @@ func TestSingleFunction(t *testing.T) {
214248
}
215249
}
216250
}
251+
252+
func TestERC20InterfaceID(t *testing.T) {
253+
contents, readErr := os.ReadFile("fixtures/abis/ERC20.json")
254+
if readErr != nil {
255+
t.Fatal("Could not read file containing ABI")
256+
}
257+
258+
decodedABI, decodeErr := Decode(contents)
259+
if decodeErr != nil {
260+
t.Fatalf("Could not decode ABI: %s", decodeErr.Error())
261+
}
262+
263+
annotations, err := Annotate(decodedABI)
264+
if err != nil {
265+
t.Fatalf("Could not generate annotations: %s", err.Error())
266+
}
267+
268+
expectedInterfaceID := "36372b07"
269+
interfaceId := hex.EncodeToString(annotations.InterfaceID)
270+
if interfaceId != expectedInterfaceID {
271+
t.Fatalf("Incorrect interface ID generated: expected: %s, actual: %s", expectedInterfaceID, interfaceId)
272+
}
273+
}

fixtures/abis/ERC20.json

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
[
2+
{
3+
"anonymous": false,
4+
"inputs": [
5+
{
6+
"indexed": true,
7+
"internalType": "address",
8+
"name": "owner",
9+
"type": "address"
10+
},
11+
{
12+
"indexed": true,
13+
"internalType": "address",
14+
"name": "spender",
15+
"type": "address"
16+
},
17+
{
18+
"indexed": false,
19+
"internalType": "uint256",
20+
"name": "value",
21+
"type": "uint256"
22+
}
23+
],
24+
"name": "Approval",
25+
"type": "event"
26+
},
27+
{
28+
"anonymous": false,
29+
"inputs": [
30+
{
31+
"indexed": true,
32+
"internalType": "address",
33+
"name": "from",
34+
"type": "address"
35+
},
36+
{
37+
"indexed": true,
38+
"internalType": "address",
39+
"name": "to",
40+
"type": "address"
41+
},
42+
{
43+
"indexed": false,
44+
"internalType": "uint256",
45+
"name": "value",
46+
"type": "uint256"
47+
}
48+
],
49+
"name": "Transfer",
50+
"type": "event"
51+
},
52+
{
53+
"inputs": [
54+
{
55+
"internalType": "address",
56+
"name": "owner",
57+
"type": "address"
58+
},
59+
{
60+
"internalType": "address",
61+
"name": "spender",
62+
"type": "address"
63+
}
64+
],
65+
"name": "allowance",
66+
"outputs": [
67+
{
68+
"internalType": "uint256",
69+
"name": "",
70+
"type": "uint256"
71+
}
72+
],
73+
"stateMutability": "view",
74+
"type": "function"
75+
},
76+
{
77+
"inputs": [
78+
{
79+
"internalType": "address",
80+
"name": "spender",
81+
"type": "address"
82+
},
83+
{
84+
"internalType": "uint256",
85+
"name": "amount",
86+
"type": "uint256"
87+
}
88+
],
89+
"name": "approve",
90+
"outputs": [
91+
{
92+
"internalType": "bool",
93+
"name": "",
94+
"type": "bool"
95+
}
96+
],
97+
"stateMutability": "nonpayable",
98+
"type": "function"
99+
},
100+
{
101+
"inputs": [
102+
{
103+
"internalType": "address",
104+
"name": "account",
105+
"type": "address"
106+
}
107+
],
108+
"name": "balanceOf",
109+
"outputs": [
110+
{
111+
"internalType": "uint256",
112+
"name": "",
113+
"type": "uint256"
114+
}
115+
],
116+
"stateMutability": "view",
117+
"type": "function"
118+
},
119+
{
120+
"inputs": [],
121+
"name": "totalSupply",
122+
"outputs": [
123+
{
124+
"internalType": "uint256",
125+
"name": "",
126+
"type": "uint256"
127+
}
128+
],
129+
"stateMutability": "view",
130+
"type": "function"
131+
},
132+
{
133+
"inputs": [
134+
{
135+
"internalType": "address",
136+
"name": "to",
137+
"type": "address"
138+
},
139+
{
140+
"internalType": "uint256",
141+
"name": "amount",
142+
"type": "uint256"
143+
}
144+
],
145+
"name": "transfer",
146+
"outputs": [
147+
{
148+
"internalType": "bool",
149+
"name": "",
150+
"type": "bool"
151+
}
152+
],
153+
"stateMutability": "nonpayable",
154+
"type": "function"
155+
},
156+
{
157+
"inputs": [
158+
{
159+
"internalType": "address",
160+
"name": "from",
161+
"type": "address"
162+
},
163+
{
164+
"internalType": "address",
165+
"name": "to",
166+
"type": "address"
167+
},
168+
{
169+
"internalType": "uint256",
170+
"name": "amount",
171+
"type": "uint256"
172+
}
173+
],
174+
"name": "transferFrom",
175+
"outputs": [
176+
{
177+
"internalType": "bool",
178+
"name": "",
179+
"type": "bool"
180+
}
181+
],
182+
"stateMutability": "nonpayable",
183+
"type": "function"
184+
}
185+
]

0 commit comments

Comments
 (0)