├── .github
├── dependabot.yml
└── workflows
│ ├── build-on-push.yml
│ ├── build-release-on-main-push.yml
│ └── dependabot-pr-auto-merge.yml
├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── net
│ └── osslabz
│ └── evm
│ └── abi
│ ├── decoder
│ ├── AbiDecoder.java
│ └── DecodedFunctionCall.java
│ ├── definition
│ ├── AbiDefinition.java
│ └── SolidityType.java
│ └── util
│ ├── ByteUtil.java
│ ├── FileUtil.java
│ └── HashUtil.java
└── test
├── java
└── net
│ └── osslabz
│ └── evm
│ └── abi
│ └── AbiDecoderTest.java
└── resources
├── abiFiles
├── SereshForwarder.json
├── TetherToken.json
├── UniswapV2Router02.json
├── UniswapV3Router.json
├── UniswapV3SwapRouter.json
├── UniswapV3SwapRouter02.json
├── ZkSync.json
├── uniswapV3Router-input
│ └── input_0xeb154fb38972106bfc0e9bce28130379c44d80be292de775e0f43e2c861e0f48
└── zkSync-input
│ └── input_0xe35a7dceb1536dfbd819ab6f756e4dcb19ea09541df54abf0f40064ba1163981
└── logback-test.xml
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "maven" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/build-on-push.yml:
--------------------------------------------------------------------------------
1 | name: build-on-push
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - main
7 |
8 | jobs:
9 | build-on-push:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: checkout
14 | uses: actions/checkout@v4
15 |
16 | - name: setup-jdk
17 | uses: actions/setup-java@v4
18 | with:
19 | java-version: 17
20 | distribution: 'temurin'
21 | cache: maven
22 |
23 | - name: maven-build-verify
24 | run: mvn --batch-mode --update-snapshots verify
--------------------------------------------------------------------------------
/.github/workflows/build-release-on-main-push.yml:
--------------------------------------------------------------------------------
1 | name: build-release-on-main-push
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build-release-on-main-push:
10 | if: ${{ !contains(github.event.head_commit.message, '[release]') }} # prevent recursive releases
11 | runs-on: ubuntu-latest
12 |
13 | permissions:
14 | contents: write
15 | packages: write
16 |
17 | steps:
18 | - name: checkout
19 | uses: actions/checkout@v4
20 | with:
21 | ref: main
22 |
23 | - name: setup-jdk
24 | uses: actions/setup-java@v4
25 | with:
26 | java-version: 21
27 | distribution: 'temurin'
28 | cache: maven
29 | server-id: ossrh
30 | server-username: MAVEN_USERNAME
31 | server-password: MAVEN_PASSWORD
32 | gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
33 | gpg-passphrase: MAVEN_GPG_PASSPHRASE
34 |
35 | - name: maven-build-verify
36 | run: mvn --batch-mode verify
37 |
38 | - name: configure-git-user
39 | uses: qoomon/actions--setup-git@v1
40 | with:
41 | user: bot
42 |
43 | - name: publish-on-maven-central
44 | run: mvn --batch-mode -P osslabz-release clean release:clean release:prepare release:perform
45 | env:
46 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
47 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
48 | MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}
49 |
50 | - name: 'get-latest-tag'
51 | id: 'get-latest-tag'
52 | uses: "WyriHaximus/github-action-get-previous-tag@v1"
53 |
54 | - name: create-release-notes
55 | uses: softprops/action-gh-release@v2
56 | with:
57 | generate_release_notes: true
58 | tag_name: ${{ steps.get-latest-tag.outputs.tag }}
59 |
60 | - name: merge-main-to-dev
61 | run: |
62 | git fetch --unshallow
63 | git checkout dev
64 | git pull
65 | git merge --no-ff main -m "[release] auto-merge released main back to dev"
66 | git push
--------------------------------------------------------------------------------
/.github/workflows/dependabot-pr-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: dependabot-pr-auto-merge
2 |
3 | on: pull_request
4 |
5 |
6 | jobs:
7 | dependabot-pr-auto-merge:
8 | runs-on: ubuntu-latest
9 |
10 | permissions:
11 | contents: write
12 | pull-requests: write
13 |
14 | if: github.actor == 'dependabot[bot]'
15 | steps:
16 | - name: dependabot-pr-fetch-metadata
17 | uses: dependabot/fetch-metadata@v2
18 |
19 | - name: dependabot-pr-approve
20 | run: gh pr review --approve "$PR_URL"
21 | env:
22 | PR_URL: ${{github.event.pull_request.html_url}}
23 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
24 |
25 | - name: dependabot-pr-auto-merge
26 | run: gh pr merge --auto --merge "$PR_URL"
27 | env:
28 | PR_URL: ${{github.event.pull_request.html_url}}
29 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Maven template
2 | target/
3 | pom.xml.tag
4 | pom.xml.releaseBackup
5 | pom.xml.versionsBackup
6 | pom.xml.next
7 | release.properties
8 | dependency-reduced-pom.xml
9 | buildNumber.properties
10 | .mvn/timing.properties
11 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar
12 | .mvn/wrapper/maven-wrapper.jar
13 |
14 | ### JetBrains template
15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
16 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
17 |
18 | # User-specific stuff
19 | .idea/**/workspace.xml
20 | .idea/**/tasks.xml
21 | .idea/**/usage.statistics.xml
22 | .idea/**/dictionaries
23 | .idea/**/shelf
24 |
25 | # Generated files
26 | .idea/**/contentModel.xml
27 |
28 | # Sensitive or high-churn files
29 | .idea/**/dataSources/
30 | .idea/**/dataSources.ids
31 | .idea/**/dataSources.local.xml
32 | .idea/**/sqlDataSources.xml
33 | .idea/**/dynamic.xml
34 | .idea/**/uiDesigner.xml
35 | .idea/**/dbnavigator.xml
36 |
37 | # Gradle
38 | .idea/**/gradle.xml
39 | .idea/**/libraries
40 |
41 | # Gradle and Maven with auto-import
42 | # When using Gradle or Maven with auto-import, you should exclude module files,
43 | # since they will be recreated, and may cause churn. Uncomment if using
44 | # auto-import.
45 | # .idea/artifacts
46 | # .idea/compiler.xml
47 | # .idea/jarRepositories.xml
48 | # .idea/modules.xml
49 | # .idea/*.iml
50 | # .idea/modules
51 | # *.iml
52 | # *.ipr
53 |
54 | # CMake
55 | cmake-build-*/
56 |
57 | # Mongo Explorer plugin
58 | .idea/**/mongoSettings.xml
59 |
60 | # File-based project format
61 | *.iws
62 |
63 | # IntelliJ
64 | out/
65 |
66 | # mpeltonen/sbt-idea plugin
67 | .idea_modules/
68 |
69 | # JIRA plugin
70 | atlassian-ide-plugin.xml
71 |
72 | # Cursive Clojure plugin
73 | .idea/replstate.xml
74 |
75 | # Crashlytics plugin (for Android Studio and IntelliJ)
76 | com_crashlytics_export_strings.xml
77 | crashlytics.properties
78 | crashlytics-build.properties
79 | fabric.properties
80 |
81 | # Editor-based Rest Client
82 | .idea/httpRequests
83 |
84 | # Android studio 3.1+ serialized cache file
85 | .idea/caches/build_file_checksums.ser
86 |
87 | ### Eclipse template
88 | .metadata
89 | bin/
90 | tmp/
91 | *.tmp
92 | *.bak
93 | *.swp
94 | *~.nib
95 | local.properties
96 | .settings/
97 | .loadpath
98 | .recommenders
99 |
100 | # External tool builders
101 | .externalToolBuilders/
102 |
103 | # Locally stored "Eclipse launch configurations"
104 | *.launch
105 |
106 | # PyDev specific (Python IDE for Eclipse)
107 | *.pydevproject
108 |
109 | # CDT-specific (C/C++ Development Tooling)
110 | .cproject
111 |
112 | # CDT- autotools
113 | .autotools
114 |
115 | # Java annotation processor (APT)
116 | .factorypath
117 |
118 | # PDT-specific (PHP Development Tools)
119 | .buildpath
120 |
121 | # sbteclipse plugin
122 | .target
123 |
124 | # Tern plugin
125 | .tern-project
126 |
127 | # TeXlipse plugin
128 | .texlipse
129 |
130 | # STS (Spring Tool Suite)
131 | .springBeans
132 |
133 | # Code Recommenders
134 | .recommenders/
135 |
136 | # Annotation Processing
137 | .apt_generated/
138 | .apt_generated_test/
139 |
140 | # Scala IDE specific (Scala & Java development for Eclipse)
141 | .cache-main
142 | .scala_dependencies
143 | .worksheet
144 |
145 | # Uncomment this line if you wish to ignore the project description file.
146 | # Typically, this file would be tracked if it contains build/dependency configurations:
147 | #.project
148 |
149 | .idea/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | EVM ABI Decoder
2 | ===============
3 | 
4 | 
5 | [](https://search.maven.org/artifact/net.osslabz/evm-abi-decoder)
6 |
7 | EVM ABI Decoder allows to decode raw input data from a EVM transaction (on Ethereum or a compatible chain like Avalanche, BSC etc.)
8 | into a processable format obtained from the contract's ABi definition (JSON).
9 |
10 | **Acknowledgement**:
11 | This project is based on [Bryce Neals's](https://github.com/prettymuchbryce) project [abidecoder](https://github.com/prettymuchbryce/abidecoder) (Kotlin), which itself is a port
12 | of [ConsenSys](https://github.com/ConsenSys) project [abi-decoder](https://github.com/ConsenSys/abi-decoder) (JavaScript).
13 |
14 | The original project is written in Kotlin, only published on [JitPack](https://jitpack.io/) (but not on [Maven Central](https://search.maven.org/)) and depends on the now
15 | deprecated [ethereumj](https://github.com/ethereum/ethereumj). These were enough reasons for me to rewrite it in Java ;-)
16 |
17 | QuickStart
18 | ---------
19 |
20 | Maven
21 | ------
22 |
23 | ```xml
24 |
25 |
26 | net.osslabz
27 | evm-abi-decoder
28 | 0.1.0
29 |
30 | ```
31 |
32 | Usage
33 | ------
34 |
35 | Loads the UniswapV2Router02 contract and parses a swap transaction:
36 |
37 | ```java
38 | // Abi can be found here: https://etherscan.io/address/0x7a250d5630b4cf539739df2c5dacb4c659f2488d#code
39 | AbiDecoder uniswapv2Abi=new AbiDecoder(this.getClass().getResource("/abiFiles/UniswapV2Router02.json").getFile());
40 |
41 | // tx: https://etherscan.io/tx/0xde2b61c91842494ac208e25a2a64d99997c382f6aaf0719d6a719b5cff1f8a07
42 | String inputData="0x18cbafe5000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000165284993ac4ac00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000d4cf8e47beac55b42ae58991785fa326d9384bd10000000000000000000000000000000000000000000000000000000062e8d8510000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
43 |
44 | /**
45 | * # Name Type Data
46 | * ----------------------------------------------------------------------
47 | * 0 amountIn uint256 10000000
48 | * 1 amountOutMin uint256 6283178947560620
49 | * 2 path address[] 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
50 | * 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
51 | * 3 to address 0xD4CF8e47BeAC55b42Ae58991785Fa326d9384Bd1
52 | * 4 deadline uint256 1659426897
53 | */
54 |
55 | DecodedFunctionCall decodedFunctionCall=uniswapv2Abi.decodeFunctionCall(inputData);
56 |
57 | System.out.println(decodedFunctionCall.getName()); // prints swapExactTokensForETH
58 |
59 | ```
60 |
61 | Logging
62 | ------
63 | This project uses slf4j-api but doesn't package an implementation. This is up to the using application. For the
64 | tests logback is backing slf4j as implementation, with a default configuration logging to STOUT.
65 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | net.osslabz
7 | evm-abi-decoder
8 | 0.1.1-SNAPSHOT
9 |
10 | ${project.groupId}:${project.artifactId}
11 | Allows to decode raw input data from an EVM smart contract call (on Ethereum or a compatible chain like
12 | Avalanche, BSC etc.) into a processable format obtained from the contract's ABi definition (JSON).
13 |
14 | https://github.com/osslabz/evm-abi-decoder
15 |
16 |
17 | UTF-8
18 | 8
19 |
20 | ${osslabz.encoding}
21 | ${osslabz.encoding}
22 |
23 | 2024-11-12T08:22:36Z
24 |
25 | ${osslabz.java.version}
26 |
27 | 1.18.38
28 |
29 |
30 |
31 |
32 | GNU General Public License 3
33 | https://www.gnu.org/licenses/gpl-3.0.html
34 |
35 |
36 |
37 |
38 |
39 | Raphael Vullriede
40 | raphael@osslabz.net
41 | osslabz.net
42 | https://www.osslabz.net
43 |
44 |
45 |
46 |
47 | scm:git:https://github.com/osslabz/evm-abi-decoder.git
48 | scm:git:https://github.com/osslabz/evm-abi-decoder.git
49 | https://github.com/osslabz/evm-abi-decoder
50 | main
51 |
52 |
53 |
54 |
55 | com.fasterxml.jackson.core
56 | jackson-databind
57 | 2.19.0
58 |
59 |
60 |
61 | org.apache.commons
62 | commons-lang3
63 | 3.17.0
64 |
65 |
66 | org.apache.commons
67 | commons-collections4
68 | 4.5.0
69 |
70 |
71 |
72 | org.bouncycastle
73 | bcprov-jdk18on
74 | 1.81
75 |
76 |
77 |
78 | org.projectlombok
79 | lombok
80 | ${lombok.version}
81 | provided
82 |
83 |
84 |
85 |
86 | org.slf4j
87 | slf4j-api
88 | 2.0.16
89 |
90 |
91 |
92 |
93 | org.junit.jupiter
94 | junit-jupiter
95 | 5.13.1
96 | test
97 |
98 |
99 |
100 | ch.qos.logback
101 | logback-classic
102 | 1.5.18
103 | test
104 |
105 |
106 |
107 |
108 |
109 | osslabz-release
110 |
111 |
112 |
113 | org.apache.maven.plugins
114 | maven-release-plugin
115 | 3.1.1
116 |
117 |
118 | nl.basjes.maven.release
119 | conventional-commits-version-policy
120 | 1.0.7
121 |
122 |
123 |
124 |
125 | true
126 | false
127 | osslabz-release
128 | @{project.version}
129 | [release]
130 | @{prefix} set version to @{releaseLabel}
131 | @{prefix} prepare for next development iteration
132 |
133 |
134 | ConventionalCommitsVersionPolicy
135 |
136 | ([0-9]+\.[0-9]+\.[0-9]+)$
137 |
138 | deploy
139 |
140 |
141 |
142 | org.apache.maven.plugins
143 | maven-javadoc-plugin
144 | 3.11.2
145 |
146 |
147 | attach-javadocs
148 |
149 | jar
150 |
151 |
152 | none
153 | false
154 |
155 |
156 |
157 |
158 |
159 | org.apache.maven.plugins
160 | maven-gpg-plugin
161 | 3.2.7
162 |
163 |
164 | sign-artifacts
165 | verify
166 |
167 | sign
168 |
169 |
170 |
171 | --pinentry-mode
172 | loopback
173 |
174 |
175 |
176 |
177 |
178 |
179 | org.sonatype.plugins
180 | nexus-staging-maven-plugin
181 | 1.7.0
182 | true
183 |
184 | ossrh
185 | https://s01.oss.sonatype.org/
186 | true
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | org.apache.maven.plugins
198 | maven-compiler-plugin
199 | 3.13.0
200 |
201 | ${maven.compiler.release}
202 |
203 |
204 | org.projectlombok
205 | lombok
206 | ${lombok.version}
207 |
208 |
209 |
210 |
211 |
212 | org.apache.maven.plugins
213 | maven-jar-plugin
214 | 3.4.2
215 |
216 |
217 | org.apache.maven.plugins
218 | maven-source-plugin
219 | 3.3.1
220 |
221 |
222 | attach-sources
223 |
224 | jar-no-fork
225 |
226 |
227 |
228 |
229 |
230 | io.github.git-commit-id
231 | git-commit-id-maven-plugin
232 | 9.0.2
233 |
234 |
235 | git-info
236 |
237 | revision
238 |
239 | initialize
240 |
241 |
242 |
243 | true
244 |
245 | git.branch
246 | ^git.build.(time|version)$
247 | ^git.commit.id.(abbrev|full)$
248 |
249 | full
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 | ossrh
258 | Maven Central
259 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
260 |
261 |
262 |
--------------------------------------------------------------------------------
/src/main/java/net/osslabz/evm/abi/decoder/AbiDecoder.java:
--------------------------------------------------------------------------------
1 | package net.osslabz.evm.abi.decoder;
2 |
3 | import lombok.Getter;
4 | import net.osslabz.evm.abi.definition.AbiDefinition;
5 | import org.bouncycastle.util.encoders.Hex;
6 |
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 | import java.nio.charset.StandardCharsets;
10 | import java.nio.file.Files;
11 | import java.nio.file.Paths;
12 | import java.util.ArrayList;
13 | import java.util.Collections;
14 | import java.util.HashMap;
15 | import java.util.List;
16 | import java.util.Map;
17 |
18 | @Getter
19 | public class AbiDecoder {
20 |
21 | protected final AbiDefinition abi;
22 | protected final Map methodSignatures = new HashMap<>();
23 |
24 | public AbiDecoder(String abiFilePath) throws IOException {
25 | this.abi = AbiDefinition.fromJson(new String(Files.readAllBytes(Paths.get(abiFilePath)), StandardCharsets.UTF_8));
26 | init();
27 | }
28 |
29 | public AbiDecoder(InputStream inputStream) {
30 | this.abi = AbiDefinition.fromJson(inputStream);
31 | init();
32 | }
33 |
34 | private void init() {
35 | for (AbiDefinition.Entry entry : this.abi) {
36 | String hexEncodedMethodSignature = Hex.toHexString(entry.encodeSignature());
37 | this.methodSignatures.put(hexEncodedMethodSignature, entry);
38 | }
39 | }
40 |
41 | public DecodedFunctionCall decodeFunctionCall(String inputData) {
42 | if (inputData == null || (inputData.startsWith("0x") && inputData.length() < 10) || inputData.length() < 8) {
43 | throw new IllegalArgumentException("Can't decode invalid input '" + inputData + "'.");
44 | }
45 | String inputNoPrefix = cleanup(inputData);
46 |
47 | String methodBytes = inputNoPrefix.substring(0, 8);
48 |
49 | if (!this.methodSignatures.containsKey(methodBytes)) {
50 | //return null;
51 | throw new IllegalStateException("Couldn't find method with signature " + methodBytes);
52 | }
53 | AbiDefinition.Entry abiEntry = this.methodSignatures.get(methodBytes);
54 |
55 | if (!(abiEntry instanceof AbiDefinition.Function)) {
56 | throw new IllegalArgumentException("Input data is not a function call, it's of type '" + abiEntry.type + "'.");
57 | }
58 |
59 | AbiDefinition.Function abiFunction = (AbiDefinition.Function) abiEntry;
60 |
61 | List params = new ArrayList<>(abiFunction.inputs.size());
62 | List> decoded = abiFunction.decode(Hex.decode(inputNoPrefix));
63 |
64 | for (int i = 0; i < decoded.size(); i++) {
65 | AbiDefinition.Entry.Param paramDefinition = abiFunction.inputs.get(i);
66 | DecodedFunctionCall.Param param = new DecodedFunctionCall.Param(paramDefinition.getName(), paramDefinition.getType().getName(), decoded.get(i));
67 | params.add(param);
68 | }
69 | return new DecodedFunctionCall(abiFunction.name, params);
70 | }
71 |
72 | public List decodeFunctionsCalls(String inputData) {
73 |
74 | DecodedFunctionCall decodedFunctionCall = this.decodeFunctionCall(inputData);
75 |
76 | List resolvedCalls = Collections.singletonList(decodedFunctionCall);
77 |
78 | if (decodedFunctionCall.getName().equalsIgnoreCase("multicall")) {
79 |
80 | DecodedFunctionCall.Param multiCallPayloadData = decodedFunctionCall.getParam("data");
81 |
82 | if (multiCallPayloadData == null) {
83 | throw new IllegalStateException("multicall function call doesn't contain expected data input param.");
84 | }
85 |
86 | resolvedCalls = new ArrayList<>();
87 | Object paramValue = multiCallPayloadData.getValue();
88 |
89 | if (paramValue instanceof String) {
90 | resolvedCalls.add(this.decodeFunctionCall((String) paramValue));
91 | } else if (paramValue instanceof byte[]) {
92 | resolvedCalls.add(this.decodeFunctionCall(Hex.toHexString((byte[]) paramValue)));
93 | } else if (paramValue instanceof Object[]) {
94 | for (Object singleCallInputData : (Object[]) paramValue) {
95 | if (singleCallInputData instanceof String) {
96 | DecodedFunctionCall call = this.decodeFunctionCall((String) singleCallInputData);
97 | if (call != null) {
98 | resolvedCalls.add(call);
99 | }
100 | } else if (singleCallInputData instanceof byte[]) {
101 | DecodedFunctionCall call = this.decodeFunctionCall(Hex.toHexString((byte[]) singleCallInputData));
102 | if (call != null) {
103 | resolvedCalls.add(call);
104 | }
105 | } else {
106 | throw new IllegalStateException("Can't decode param name=" + multiCallPayloadData.getName() + ", type=" + multiCallPayloadData.getType() + ", value=" + multiCallPayloadData.getValue());
107 | }
108 | }
109 | } else {
110 | throw new IllegalStateException("Can't decode param name=" + multiCallPayloadData.getName() + ", type=" + multiCallPayloadData.getType() + ", value=" + multiCallPayloadData.getValue());
111 | }
112 | }
113 | return resolvedCalls;
114 | }
115 |
116 |
117 | public DecodedFunctionCall decodeLogEvent(List topics, String data) {
118 | if (topics.isEmpty()) {
119 | throw new IllegalArgumentException("Log.topics is empty");
120 | }
121 | String funcSignature = cleanup(topics.get(0));
122 | AbiDefinition.Entry abiEntry = methodSignatures.get(funcSignature);
123 | if (abiEntry == null) {
124 | throw new IllegalStateException("Couldn't find method with signature " + funcSignature);
125 | } else {
126 | if (abiEntry instanceof AbiDefinition.Event) {
127 | AbiDefinition.Event abiEvent = (AbiDefinition.Event) abiEntry;
128 | List> decoded = abiEvent.decode(hexBytes(data), topics
129 | .stream()
130 | .map(AbiDecoder::hexBytes)
131 | .toArray(byte[][]::new));
132 | List params = new ArrayList<>(abiEvent.inputs.size());
133 | for (int i = 0; i < decoded.size(); i++) {
134 | AbiDefinition.Entry.Param paramDefinition = abiEvent.inputs.get(i);
135 | DecodedFunctionCall.Param param = new DecodedFunctionCall.Param(paramDefinition.getName(), paramDefinition.getType()
136 | .getName(), decoded.get(i));
137 | params.add(param);
138 | }
139 | return new DecodedFunctionCall(abiEvent.name, params);
140 | } else {
141 | throw new IllegalArgumentException("Input data is not a event, it's of type '" + abiEntry.type + "'.");
142 | }
143 | }
144 | }
145 |
146 | private static String cleanup(String hex) {
147 | return hex.startsWith("0x") ? hex.substring(2) : hex;
148 | }
149 |
150 | private static byte[] hexBytes(String hex) {
151 | return Hex.decode(cleanup(hex));
152 | }
153 | }
--------------------------------------------------------------------------------
/src/main/java/net/osslabz/evm/abi/decoder/DecodedFunctionCall.java:
--------------------------------------------------------------------------------
1 | package net.osslabz.evm.abi.decoder;
2 |
3 | import lombok.Data;
4 | import net.osslabz.evm.abi.util.ByteUtil;
5 |
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 | import java.util.Collection;
9 | import java.util.LinkedHashMap;
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 |
14 | @Data
15 | public class DecodedFunctionCall {
16 |
17 | private String name;
18 | private Map params;
19 |
20 | public DecodedFunctionCall(String name, List params) {
21 | this.name = name;
22 | this.params = new LinkedHashMap<>();
23 | for (Param param : params) {
24 | this.params.put(param.getName().toLowerCase(), param);
25 | }
26 | }
27 |
28 | public Param getParam(String paramName) {
29 | return this.params.get(paramName.toLowerCase());
30 | }
31 |
32 | public Map params() {
33 | return this.params;
34 | }
35 |
36 | public Collection getParams() {
37 | return this.params.values();
38 | }
39 |
40 | public List getParamList() {
41 | return new ArrayList<>(this.getParams());
42 | }
43 |
44 | public int getSize() {
45 | return this.params.size();
46 | }
47 |
48 |
49 | @Data
50 | public static class Param {
51 | private String name;
52 | private String type;
53 | private Object value;
54 |
55 | public Param(String name, String type, Object value) {
56 | this.name = name;
57 | this.type = type;
58 | if (value instanceof byte[]) {
59 | this.value = "0x" + ByteUtil.toHexString((byte[]) value);
60 | } else if (value instanceof Object[]) {
61 | Object[] valueAsObjectArray = (Object[]) value;
62 | this.value = new Object[valueAsObjectArray.length];
63 | for (int i = 0; i < valueAsObjectArray.length; i++) {
64 | Object o = valueAsObjectArray[i];
65 | ((Object[]) this.value)[i] = o instanceof byte[] ? "0x" + ByteUtil.toHexString((byte[]) o) : o;
66 | }
67 | } else {
68 | this.value = value;
69 | }
70 | }
71 |
72 | public String toString() {
73 | String valueString = this.value == null ? "null" : (this.value.getClass().isArray() ? Arrays.toString((Object[]) this.value) : this.value.toString());
74 | return this.getClass().getName() + "(name=" + this.name + ", type=" + this.getType() + ", value=" + valueString + ")";
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/main/java/net/osslabz/evm/abi/definition/AbiDefinition.java:
--------------------------------------------------------------------------------
1 | package net.osslabz.evm.abi.definition;
2 |
3 | import com.fasterxml.jackson.annotation.JsonCreator;
4 | import com.fasterxml.jackson.annotation.JsonInclude;
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.fasterxml.jackson.core.JsonProcessingException;
7 | import com.fasterxml.jackson.databind.DeserializationFeature;
8 | import com.fasterxml.jackson.databind.ObjectMapper;
9 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
10 | import com.fasterxml.jackson.databind.util.StdConverter;
11 | import lombok.Data;
12 | import net.osslabz.evm.abi.util.ByteUtil;
13 | import net.osslabz.evm.abi.util.HashUtil;
14 | import org.apache.commons.collections4.CollectionUtils;
15 | import org.apache.commons.collections4.Predicate;
16 | import org.apache.commons.lang3.StringUtils;
17 |
18 | import java.io.IOException;
19 | import java.io.InputStream;
20 | import java.io.Reader;
21 | import java.util.ArrayList;
22 | import java.util.List;
23 | import java.util.stream.Collectors;
24 |
25 | import static com.fasterxml.jackson.annotation.JsonInclude.Include;
26 | import static java.lang.String.format;
27 | import static net.osslabz.evm.abi.definition.SolidityType.IntType.decodeInt;
28 | import static org.apache.commons.collections4.ListUtils.select;
29 | import static org.apache.commons.lang3.ArrayUtils.subarray;
30 | import static org.apache.commons.lang3.StringUtils.join;
31 | import static org.apache.commons.lang3.StringUtils.stripEnd;
32 |
33 | public class AbiDefinition extends ArrayList {
34 | private final static ObjectMapper DEFAULT_MAPPER = new ObjectMapper()
35 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
36 | .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
37 |
38 | public static class ParamSanitizer extends StdConverter {
39 | public ParamSanitizer() {
40 | }
41 |
42 | @Override
43 | public Entry.Param convert(Entry.Param param) {
44 | if (param.type instanceof SolidityType.TupleType) {
45 | for (Entry.Param c : param.components) {
46 | ((SolidityType.TupleType) param.type).getTypes().add(c.getType());
47 | }
48 | } else if (param.type instanceof SolidityType.ArrayType) {
49 | SolidityType.ArrayType arrayType = (SolidityType.ArrayType) param.type;
50 | if (arrayType.elementType instanceof SolidityType.TupleType) {
51 | for (AbiDefinition.Entry.Param c : param.components) {
52 | ((SolidityType.TupleType) arrayType.elementType).getTypes().add(c.getType());
53 | }
54 | }
55 | }
56 | return param;
57 | }
58 | }
59 |
60 | public static AbiDefinition fromJson(String json) {
61 | try {
62 | return DEFAULT_MAPPER.readValue(json, AbiDefinition.class);
63 | } catch (IOException e) {
64 | throw new RuntimeException(e);
65 | }
66 | }
67 |
68 | public static AbiDefinition fromJson(Reader reader) {
69 | try {
70 | return DEFAULT_MAPPER.readValue(reader, AbiDefinition.class);
71 | } catch (IOException e) {
72 | throw new RuntimeException(e);
73 | }
74 | }
75 |
76 | public static AbiDefinition fromJson(InputStream inputStream) {
77 | try {
78 | return DEFAULT_MAPPER.readValue(inputStream, AbiDefinition.class);
79 | } catch (IOException e) {
80 | throw new RuntimeException(e);
81 | }
82 | }
83 |
84 | public String toJson() {
85 | try {
86 | return new ObjectMapper().writeValueAsString(this);
87 | } catch (JsonProcessingException e) {
88 | throw new RuntimeException(e);
89 | }
90 | }
91 |
92 | private T find(Class resultClass, final Entry.Type type, final Predicate searchPredicate) {
93 | return (T) CollectionUtils.find(this, entry -> entry.type == type && searchPredicate.evaluate((T) entry));
94 | }
95 |
96 | public Function findFunction(Predicate searchPredicate) {
97 | return find(Function.class, Entry.Type.function, searchPredicate);
98 | }
99 |
100 | public Event findEvent(Predicate searchPredicate) {
101 | return find(Event.class, Entry.Type.event, searchPredicate);
102 | }
103 |
104 | public Error findError(Predicate searchError) {
105 | return find(Error.class, Entry.Type.error, searchError);
106 | }
107 |
108 | public Constructor findConstructor() {
109 | return find(Constructor.class, Entry.Type.constructor, object -> true);
110 | }
111 |
112 | @Override
113 | public String toString() {
114 | return toJson();
115 | }
116 |
117 |
118 | @JsonInclude(Include.NON_NULL)
119 | public static abstract class Entry {
120 |
121 | public final Boolean anonymous;
122 | public final Boolean constant;
123 | public final String name;
124 | public final List inputs;
125 | public final List outputs;
126 | public final Type type;
127 | public final Boolean payable;
128 |
129 | public Entry(Boolean anonymous, Boolean constant, String name, List inputs, List outputs, Type type, Boolean payable) {
130 | this.anonymous = anonymous;
131 | this.constant = constant;
132 | this.name = name;
133 | this.inputs = inputs;
134 | this.outputs = outputs;
135 | this.type = type;
136 | this.payable = payable;
137 | }
138 |
139 | @JsonCreator
140 | public static Entry create(@JsonProperty("anonymous") boolean anonymous,
141 | @JsonProperty("constant") boolean constant,
142 | @JsonProperty("name") String name,
143 | @JsonProperty("inputs") List inputs,
144 | @JsonProperty("outputs") List outputs,
145 | @JsonProperty("type") Type type,
146 | @JsonProperty(value = "payable", required = false, defaultValue = "false") Boolean payable) {
147 | Entry result = null;
148 | switch (type) {
149 | case constructor:
150 | result = new Constructor(inputs, outputs);
151 | break;
152 | case function:
153 | case fallback:
154 | result = new Function(constant, name, inputs, outputs, payable);
155 | break;
156 | case receive:
157 | result = new Function(constant, name, inputs, outputs, payable);
158 | break;
159 | case event:
160 | result = new Event(anonymous, name, inputs, outputs);
161 | break;
162 | case error:
163 | result = new Error(name, inputs);
164 | break;
165 | }
166 |
167 | return result;
168 | }
169 |
170 | public String formatSignature() {
171 | StringBuilder paramsTypes = new StringBuilder();
172 | if (inputs != null) {
173 | for (Param param : inputs) {
174 | String type = formatParamSignature(param);
175 | paramsTypes.append(type).append(",");
176 | }
177 | }
178 |
179 | return format("%s(%s)", name, stripEnd(paramsTypes.toString(), ","));
180 | }
181 |
182 | public String formatParamSignature(Param param) {
183 | String type = param.type.getCanonicalName();
184 | if (param.type instanceof SolidityType.TupleType) {
185 | type = "(" + StringUtils.join(param.getComponents().stream().map(this::formatParamSignature).collect(Collectors.toList()), ",") + ")";
186 | } else if (param.type instanceof SolidityType.ArrayType && ((SolidityType.ArrayType)param.type).elementType instanceof SolidityType.TupleType) {
187 | type = "(" + StringUtils.join(param.getComponents().stream().map(this::formatParamSignature).collect(Collectors.toList()), ",") + ")[]";
188 | }
189 | return type;
190 | }
191 |
192 | public byte[] fingerprintSignature() {
193 | return HashUtil.hashAsKeccak(formatSignature().getBytes());
194 | }
195 |
196 | public byte[] encodeSignature() {
197 | return fingerprintSignature();
198 | }
199 |
200 | public enum Type {
201 | constructor,
202 | function,
203 | event,
204 | fallback,
205 | receive,
206 | error
207 | }
208 |
209 | @Data
210 | @JsonInclude(Include.NON_NULL)
211 | @JsonDeserialize(converter = ParamSanitizer.class) // invoked after class is fully deserialized
212 | public static class Param {
213 | private Boolean indexed;
214 | private String name;
215 | private SolidityType type;
216 |
217 | private List components;
218 |
219 | public static List> decodeList(List params, byte[] encoded) {
220 | List