diff --git a/Cargo.lock b/Cargo.lock index 6ed09954..a8146c49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,22 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aho-corasick" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "anyhow" version = "1.0.31" @@ -114,6 +131,34 @@ dependencies = [ "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "curl" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "curl-sys 0.4.31+curl-7.70.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.57 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "socket2 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "curl-sys" +version = "0.4.31+curl-7.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.57 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dtoa" version = "0.4.5" @@ -204,11 +249,44 @@ name = "ethers-contract" version = "0.1.0" dependencies = [ "ethers-abi 0.1.0", + "ethers-contract-abigen 0.1.0", + "ethers-contract-derive 0.1.0", "ethers-providers 0.1.0", "ethers-signers 0.1.0", "ethers-types 0.1.0", + "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ethers-contract-abigen" +version = "0.1.0" +dependencies = [ + "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "curl 0.4.29 (registry+https://github.com/rust-lang/crates.io-index)", + "ethers-abi 0.1.0", + "ethers-types 0.1.0", + "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ethers-contract-derive" +version = "0.1.0" +dependencies = [ + "ethers-abi 0.1.0", + "ethers-contract-abigen 0.1.0", + "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -216,6 +294,7 @@ name = "ethers-providers" version = "0.1.0" dependencies = [ "async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "ethers-abi 0.1.0", "ethers-types 0.1.0", "ethers-utils 0.1.0", "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -521,6 +600,17 @@ name = "libc" version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "libz-sys" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "log" version = "0.4.8" @@ -602,6 +692,18 @@ name = "openssl-probe" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "openssl-sys" +version = "0.9.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "parity-scale-codec" version = "1.3.0" @@ -646,6 +748,11 @@ name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ppv-lite86" version = "0.2.8" @@ -825,6 +932,27 @@ dependencies = [ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "regex" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "reqwest" version = "0.10.4" @@ -1021,6 +1149,17 @@ name = "smallvec" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "spin" version = "0.5.2" @@ -1059,6 +1198,14 @@ dependencies = [ "syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "time" version = "0.1.43" @@ -1200,6 +1347,11 @@ dependencies = [ "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "vcpkg" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "version_check" version = "0.9.2" @@ -1359,6 +1511,8 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] +"checksum Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" "checksum anyhow 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" "checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "26c4f3195085c36ea8d24d32b2f828d23296a9370a28aa39d111f6f16bef9f3b" @@ -1378,6 +1532,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" "checksum crunchy 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" "checksum ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +"checksum curl 0.4.29 (registry+https://github.com/rust-lang/crates.io-index)" = "762e34611d2d5233a506a79072be944fddd057db2f18e04c0d6fa79e3fd466fd" +"checksum curl-sys 0.4.31+curl-7.70.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dcd62757cc4f5ab9404bc6ca9f0ae447e729a1403948ce5106bd588ceac6a3b0" "checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" @@ -1413,6 +1569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)" = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" +"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" @@ -1423,12 +1580,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum net2 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" "checksum once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +"checksum openssl-sys 0.9.57 (registry+https://github.com/rust-lang/crates.io-index)" = "7410fef80af8ac071d4f63755c0ab89ac3df0fd1ea91f1d1f37cf5cec4395990" "checksum parity-scale-codec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "329c8f7f4244ddb5c37c103641027a76c530e65e8e4b8240b29f81ea40508b17" "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum pin-project 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "edc93aeee735e60ecb40cf740eb319ff23eab1c5748abfdb5c180e4ce49f7791" "checksum pin-project-internal 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "e58db2081ba5b4c93bd6be09c40fd36cb9193a8336c384f3b40012e531aa7e40" "checksum pin-project-lite 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" "checksum pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" "checksum primitive-types 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c55c21c64d0eaa4d7ed885d959ef2d62d9e488c27c0e02d9aa5ce6c877b7d5f8" "checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" @@ -1450,6 +1609,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" +"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" "checksum reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b81e49ddec5109a9dcfc5f2a317ff53377c915e9ae9d4f2fb50914b85614e2" "checksum ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c" "checksum rlp 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4a7d3f9bed94764eac15b8f14af59fac420c236adaff743b7bcc88e265cb4345" @@ -1469,11 +1631,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" +"checksum socket2 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)" = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" "checksum syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f14a640819f79b72a710c0be059dce779f9339ae046c8bef12c361d56702146f" "checksum thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344" "checksum thiserror-impl 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "893582086c2f98cde18f906265a65b5030a074b1046c674ae898be6519a7f479" +"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" "checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" "checksum tiny-keccak 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d8a021c69bb74a44ccedb824a046447e2c84a01df9e5c20779750acb38e11b2" "checksum tiny-keccak 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" @@ -1490,6 +1654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" "checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/Cargo.toml b/Cargo.toml index bc52e772..abd2dbb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "./crates/ethers", "./crates/ethers-abi", "./crates/ethers-contract", - # "./crates/ethers-derive", "./crates/ethers-providers", "./crates/ethers-signers", "./crates/ethers-types", diff --git a/README.md b/README.md index 5db323cb..c71bb97e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Complete Ethereum wallet implementation and utilities in Rust (with WASM and FFI - [ ] Deploy and interact with smart contracts - [ ] Type safe smart contract bindings - [ ] Hardware wallet support +- [ ] CLI for creating transactions, interacting with contracts, generating bindings from ABIs (abigen equivalent), ... - [ ] ... ## Directory Structure diff --git a/crates/ethers-abi/src/tokens.rs b/crates/ethers-abi/src/tokens.rs index d6bbb234..66358e2c 100644 --- a/crates/ethers-abi/src/tokens.rs +++ b/crates/ethers-abi/src/tokens.rs @@ -9,7 +9,7 @@ use thiserror::Error; #[derive(Clone, Debug, Error)] #[error("{0}")] -pub struct InvalidOutputType(String); +pub struct InvalidOutputType(pub String); /// Output type possible to deserialize from Contract ABI pub trait Detokenize { diff --git a/crates/ethers-contract/Cargo.toml b/crates/ethers-contract/Cargo.toml index 6933f80f..0d24209b 100644 --- a/crates/ethers-contract/Cargo.toml +++ b/crates/ethers-contract/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] ethers-contract-abigen = { path = "ethers-contract-abigen", optional = true } +ethers-contract-derive = { path = "ethers-contract-derive", optional = true } ethers-abi = { path = "../ethers-abi" } ethers-providers = { path = "../ethers-providers" } @@ -15,7 +16,8 @@ ethers-types = { path = "../ethers-types" } serde = { version = "1.0.110", default-features = false } rustc-hex = { version = "2.1.0", default-features = false } thiserror = { version = "1.0.19", default-features = false } +once_cell = "1.4.0" [features] -default = [] -abigen = ["ethers-contract-abigen"] +default = ["abigen"] +abigen = ["ethers-contract-abigen", "ethers-contract-derive"] diff --git a/crates/ethers-derive/Cargo.toml b/crates/ethers-contract/ethers-contract-abigen/Cargo.toml similarity index 64% rename from crates/ethers-derive/Cargo.toml rename to crates/ethers-contract/ethers-contract-abigen/Cargo.toml index b2e6ba9f..7aeb763a 100644 --- a/crates/ethers-derive/Cargo.toml +++ b/crates/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -1,16 +1,14 @@ [package] -name = "ethers-derive" +name = "ethers-contract-abigen" version = "0.1.0" authors = ["Nicholas Rodrigues Lordello ", "Georgios Konstantopoulos "] edition = "2018" license = "MIT OR Apache-2.0" -repository = "https://github.com/gnosis/ethcontract-rs" -homepage = "https://github.com/gnosis/ethcontract-rs" -documentation = "https://docs.rs/ethcontract" description = "Code generation for type-safe bindings to Ethereum smart contracts" [dependencies] -ethers-abi = { path = "../ethers-abi" } +ethers-abi = { path = "../../ethers-abi" } +ethers-types = { path = "../../ethers-types" } anyhow = "1.0" curl = "0.4" @@ -19,3 +17,5 @@ proc-macro2 = "1.0" quote = "1.0" syn = "1.0.12" url = "2.1" +serde_json = "1.0.53" +once_cell = "1.4.0" diff --git a/crates/ethers-derive/src/README.md b/crates/ethers-contract/ethers-contract-abigen/README.md similarity index 100% rename from crates/ethers-derive/src/README.md rename to crates/ethers-contract/ethers-contract-abigen/README.md diff --git a/crates/ethers-contract/ethers-contract-abigen/examples/abigen.rs b/crates/ethers-contract/ethers-contract-abigen/examples/abigen.rs new file mode 100644 index 00000000..1ee86f68 --- /dev/null +++ b/crates/ethers-contract/ethers-contract-abigen/examples/abigen.rs @@ -0,0 +1,12 @@ +use ethers_contract_abigen::Builder; + +const ABI: &'static str = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#; + +fn main() { + let builder = Builder::from_str("ERC20Token", ABI); + builder + .generate() + .unwrap() + .write_to_file("token.rs") + .unwrap(); +} diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract.rs new file mode 100644 index 00000000..97d52289 --- /dev/null +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -0,0 +1,161 @@ +#![deny(missing_docs)] + +//! Crate for generating type-safe bindings to Ethereum smart contracts. This +//! crate is intended to be used either indirectly with the `ethcontract` +//! crate's `contract` procedural macro or directly from a build script. + +mod common; +mod events; +mod methods; +mod types; + +use super::util; +use super::Args; +use anyhow::{anyhow, Context as _, Result}; +use ethers_abi::Abi; +use ethers_types::Address; +use inflector::Inflector; +use proc_macro2::{Ident, Literal, TokenStream}; +use quote::quote; +use std::collections::HashMap; +use syn::{Path, Visibility}; + +/// Internal shared context for generating smart contract bindings. +pub(crate) struct Context { + /// The contract name + name: String, + + /// The ABI string pre-parsing. + abi_str: Literal, + + /// The parsed ABI. + abi: Abi, + + /// The identifier for the runtime crate. Usually this is `ethcontract` but + /// it can be different if the crate was renamed in the Cargo manifest for + /// example. + runtime_crate: Ident, + + /// The visibility for the generated module and re-exported contract type. + visibility: Visibility, + + /// The contract name as an identifier. + contract_name: Ident, + + /// Manually specified method aliases. + method_aliases: HashMap, + + /// Derives added to event structs and enums. + event_derives: Vec, +} + +impl Context { + pub fn expand(args: Args) -> Result { + let cx = Self::from_args(args)?; + let name = &cx.contract_name; + let name_mod = util::ident(&format!( + "{}_mod", + cx.contract_name.to_string().to_lowercase() + )); + + // 0. Imports + let imports = common::imports(); + + // 1. Declare Contract struct + let struct_decl = common::struct_declaration(&cx); + + // 2. Declare events structs & impl FromTokens for each event + let events_decl = cx.events_declaration()?; + + // 3. impl block for the event functions + let contract_events = cx.events()?; + + // 4. impl block for the contract methods + let contract_methods = cx.methods()?; + + Ok(quote! { + // export all the created data types + pub use #name_mod::*; + + mod #name_mod { + #imports + + #struct_decl + + impl<'a, S: Signer, P: JsonRpcClient> #name<'a, S, P> { + /// Creates a new contract instance with the specified `ethers` + /// client at the given `Address`. The contract derefs to a `ethers::Contract` + /// object + pub fn new>(address: T, client: &'a Client<'a, S, P>) -> Self { + let contract = Contract::new(client, &ABI, address.into()); + Self(contract) + } + + // TODO: Implement deployment. + + #contract_methods + + #contract_events + } + + #events_decl + } + }) + } + + /// Create a context from the code generation arguments. + fn from_args(args: Args) -> Result { + // get the actual ABI string + let abi_str = args.abi_source.get().context("failed to get ABI JSON")?; + + // parse it + let abi: Abi = serde_json::from_str(&abi_str) + .with_context(|| format!("invalid artifact JSON '{}'", abi_str)) + .with_context(|| { + format!("failed to parse artifact from source {:?}", args.abi_source,) + })?; + + let raw_contract_name = args.contract_name; + + let runtime_crate = util::ident(&args.runtime_crate_name); + + let visibility = match args.visibility_modifier.as_ref() { + Some(vis) => syn::parse_str(vis)?, + None => Visibility::Inherited, + }; + + let contract_name = util::ident(&raw_contract_name); + + // NOTE: We only check for duplicate signatures here, since if there are + // duplicate aliases, the compiler will produce a warning because a + // method will be re-defined. + let mut method_aliases = HashMap::new(); + for (signature, alias) in args.method_aliases.into_iter() { + let alias = syn::parse_str(&alias)?; + if method_aliases.insert(signature.clone(), alias).is_some() { + return Err(anyhow!( + "duplicate method signature '{}' in method aliases", + signature, + )); + } + } + + let event_derives = args + .event_derives + .iter() + .map(|derive| syn::parse_str::(derive)) + .collect::, _>>() + .context("failed to parse event derives")?; + + Ok(Context { + name: raw_contract_name, + abi, + abi_str: Literal::string(&abi_str), + runtime_crate, + visibility, + contract_name, + method_aliases, + event_derives, + }) + } +} diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs new file mode 100644 index 00000000..6bdc1ab1 --- /dev/null +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -0,0 +1,49 @@ +use super::Context; + +use ethers_types::Address; +use proc_macro2::{Literal, TokenStream}; +use quote::quote; + +pub(crate) fn imports() -> TokenStream { + quote! { + use ethers_contract::{ + Sender, Event, + abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable}, + Contract, Lazy, + types::*, // import all the types so that we can codegen for everything + signers::{Signer, Client}, + providers::JsonRpcClient, + }; + } +} + +pub(crate) fn struct_declaration(cx: &Context) -> TokenStream { + let name = &cx.contract_name; + let abi = &cx.abi_str; + + quote! { + // Inline ABI declaration + static ABI: Lazy = Lazy::new(|| serde_json::from_str(#abi) + .expect("invalid abi")); + + // Struct declaration + #[derive(Clone)] + pub struct #name<'a, S, P>(Contract<'a, S, P>); + + + // Deref to the inner contract in order to access more specific functions functions + impl<'a, S, P> std::ops::Deref for #name<'a, S, P> { + type Target = Contract<'a, S, P>; + + fn deref(&self) -> &Self::Target { &self.0 } + } + + impl<'a, S: Signer, P: JsonRpcClient> std::fmt::Debug for #name<'a, S, P> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple(stringify!(#name)) + .field(&self.address()) + .finish() + } + } + } +} diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs new file mode 100644 index 00000000..c58a4a69 --- /dev/null +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -0,0 +1,566 @@ +use super::{types, util, Context}; +use ethers_abi::{Event, EventExt, EventParam, Hash, ParamType}; + +use anyhow::Result; +use inflector::Inflector; +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use syn::Path; + +impl Context { + /// Expands each event to a struct + its impl Detokenize block + pub fn events_declaration(&self) -> Result { + let data_types = self + .abi + .events() + .map(|event| expand_event(event, &self.event_derives)) + .collect::>>()?; + + if data_types.is_empty() { + return Ok(quote! {}); + } + + Ok(quote! { + #( #data_types )* + }) + } + + pub fn events(&self) -> Result { + let data_types = self + .abi + .events() + .map(|event| expand_filter(event)) + .collect::>>()?; + + if data_types.is_empty() { + return Ok(quote! {}); + } + + Ok(quote! { + #( #data_types )* + }) + } +} + +/// Expands into a single method for contracting an event stream. +fn expand_filter(event: &Event) -> Result { + let name = util::safe_ident(&event.name.to_snake_case()); + let ev_name = Literal::string(&event.name); + let result = util::ident(&event.name.to_pascal_case()); + + let doc = util::expand_doc(&format!("Gets the {} event", event.name)); + Ok(quote! { + + #doc + pub fn #name<'b>(&'a self) -> Event<'a, 'b, P, #result> where 'a: 'b { + self.0.event(#ev_name).expect("event not found (this should never happen)") + } + }) +} + +/// Expands an ABI event into a single event data type. This can expand either +/// into a structure or a tuple in the case where all event parameters (topics +/// and data) are anonymous. +fn expand_event(event: &Event, event_derives: &[Path]) -> Result { + let event_name = expand_struct_name(event); + + let signature = expand_hash(event.signature()); + + let abi_signature = event.abi_signature(); + let abi_signature_lit = Literal::string(&abi_signature); + let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature)); + + let params = expand_params(event)?; + + // expand as a tuple if all fields are anonymous + let all_anonymous_fields = event.inputs.iter().all(|input| input.name.is_empty()); + let (data_type_definition, data_type_construction) = if all_anonymous_fields { + expand_data_tuple(&event_name, ¶ms) + } else { + expand_data_struct(&event_name, ¶ms) + }; + + // read each token parameter as the required data type + let params_len = Literal::usize_unsuffixed(params.len()); + let read_param_token = params + .iter() + .map(|(name, ty)| { + quote! { + let #name = #ty::from_token(tokens.next().expect("this should never happen"))?; + } + }) + .collect::>(); + + let derives = expand_derives(event_derives); + + Ok(quote! { + #[derive(Clone, Debug, Default, Eq, PartialEq, #derives)] + pub #data_type_definition + + impl #event_name { + /// Retrieves the signature for the event this data corresponds to. + /// This signature is the Keccak-256 hash of the ABI signature of + /// this event. + pub const fn signature() -> H256 { + #signature + } + + /// Retrieves the ABI signature for the event this data corresponds + /// to. For this event the value should always be: + /// + #abi_signature_doc + pub const fn abi_signature() -> &'static str { + #abi_signature_lit + } + } + + impl Detokenize for #event_name { + fn from_tokens( + tokens: Vec, + ) -> Result { + if tokens.len() != #params_len { + return Err(InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + #params_len, + tokens.len(), + tokens + ))); + } + + #[allow(unused_mut)] + let mut tokens = tokens.into_iter(); + #( #read_param_token )* + + Ok(#data_type_construction) + } + } + }) +} + +/// Expands an ABI event into an identifier for its event data type. +fn expand_struct_name(event: &Event) -> TokenStream { + let event_name = util::ident(&event.name.to_pascal_case()); + quote! { #event_name } +} + +/// Expands an ABI event into name-type pairs for each of its parameters. +fn expand_params(event: &Event) -> Result> { + event + .inputs + .iter() + .enumerate() + .map(|(i, input)| { + // NOTE: Events can contain nameless values. + let name = util::expand_input_name(i, &input.name); + let ty = expand_input_type(&input)?; + + Ok((name, ty)) + }) + .collect() +} + +/// Expands an event data structure from its name-type parameter pairs. Returns +/// a tuple with the type definition (i.e. the struct declaration) and +/// construction (i.e. code for creating an instance of the event data). +fn expand_data_struct( + name: &TokenStream, + params: &[(TokenStream, TokenStream)], +) -> (TokenStream, TokenStream) { + let fields = params + .iter() + .map(|(name, ty)| quote! { pub #name: #ty }) + .collect::>(); + + let param_names = params + .iter() + .map(|(name, _)| name) + .cloned() + .collect::>(); + + let definition = quote! { struct #name { #( #fields, )* } }; + let construction = quote! { #name { #( #param_names ),* } }; + + (definition, construction) +} + +/// Expands an event data named tuple from its name-type parameter pairs. +/// Returns a tuple with the type definition and construction. +fn expand_data_tuple( + name: &TokenStream, + params: &[(TokenStream, TokenStream)], +) -> (TokenStream, TokenStream) { + let fields = params + .iter() + .map(|(_, ty)| quote! { pub #ty }) + .collect::>(); + + let param_names = params + .iter() + .map(|(name, _)| name) + .cloned() + .collect::>(); + + let definition = quote! { struct #name( #( #fields ),* ); }; + let construction = quote! { #name( #( #param_names ),* ) }; + + (definition, construction) +} + +/// Expands an ABI event into filter methods for its indexed parameters. +fn expand_builder_topic_filters(event: &Event) -> Result { + let topic_filters = event + .inputs + .iter() + .filter(|input| input.indexed) + .enumerate() + .map(|(topic_index, input)| expand_builder_topic_filter(topic_index, input)) + .collect::>>()?; + + Ok(quote! { + #( #topic_filters )* + }) +} + +/// Expands a event parameter into an event builder filter method for the +/// specified topic index. +fn expand_builder_topic_filter(topic_index: usize, param: &EventParam) -> Result { + let doc = util::expand_doc(&format!( + "Adds a filter for the `{}` event parameter.", + param.name, + )); + let topic = util::ident(&format!("topic{}", topic_index)); + let name = if param.name.is_empty() { + topic.clone() + } else { + util::safe_ident(¶m.name.to_snake_case()) + }; + let ty = expand_input_type(¶m)?; + + Ok(quote! { + #doc + pub fn #name(mut self, topic: Topic<#ty>) -> Self { + self.0 = (self.0).#topic(topic); + self + } + }) +} + +/// Expands an ABI event into an identifier for its event data type. +fn expand_builder_name(event: &Event) -> TokenStream { + let builder_name = util::ident(&format!("{}Builder", &event.name.to_pascal_case())); + quote! { #builder_name } +} + +fn expand_derives(derives: &[Path]) -> TokenStream { + quote! {#(#derives),*} +} + +/// Expands an event property type. +/// +/// Note that this is slightly different than an expanding a Solidity type as +/// complex types like arrays and strings get emited as hashes when they are +/// indexed. +fn expand_input_type(input: &EventParam) -> Result { + Ok(match (&input.kind, input.indexed) { + (ParamType::Array(..), true) + | (ParamType::Bytes, true) + | (ParamType::FixedArray(..), true) + | (ParamType::String, true) + | (ParamType::Tuple(..), true) => { + quote! { H256 } + } + (kind, _) => types::expand(kind)?, + }) +} + +/// Expands a 256-bit `Hash` into a literal representation that can be used with +/// quasi-quoting for code generation. We do this to avoid allocating at runtime +fn expand_hash(hash: Hash) -> TokenStream { + let bytes = hash.as_bytes().iter().copied().map(Literal::u8_unsuffixed); + + quote! { + H256([#( #bytes ),*]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ethers_abi::{EventParam, ParamType}; + + // #[test] + // fn expand_empty_filters() { + // assert_quote!(expand_filters(&Context::default()).unwrap(), {}); + // } + + #[test] + fn expand_transfer_filter() { + let event = Event { + name: "Transfer".into(), + inputs: vec![ + EventParam { + name: "from".into(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "to".into(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "amount".into(), + kind: ParamType::Uint(256), + indexed: false, + }, + ], + anonymous: false, + }; + let signature = expand_hash(event.signature()); + + assert_quote!(expand_filter(&event).unwrap(), { + /// Generated by `ethcontract`. + pub fn transfer(&self) -> self::event_builders::TransferBuilder { + self::event_builders::TransferBuilder( + self.instance.event(#signature) + .expect("generated event filter"), + ) + } + }); + } + + #[test] + fn expand_transfer_builder_topic_filters() { + let event = Event { + name: "Transfer".into(), + inputs: vec![ + EventParam { + name: "from".into(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "to".into(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "amount".into(), + kind: ParamType::Uint(256), + indexed: false, + }, + ], + anonymous: false, + }; + + #[rustfmt::skip] + assert_quote!(expand_builder_topic_filters(&event).unwrap(), { + #[doc = "Adds a filter for the from event parameter."] + pub fn from(mut self, topic: self::ethcontract::Topic) -> Self { + self.0 = (self.0).topic0(topic); + self + } + + #[doc = "Adds a filter for the to event parameter."] + pub fn to(mut self, topic: self::ethcontract::Topic) -> Self { + self.0 = (self.0).topic1(topic); + self + } + }); + } + + #[test] + fn expand_data_struct_value() { + let event = Event { + name: "Foo".into(), + inputs: vec![ + EventParam { + name: "a".into(), + kind: ParamType::Bool, + indexed: false, + }, + EventParam { + name: String::new(), + kind: ParamType::Address, + indexed: false, + }, + ], + anonymous: false, + }; + + let name = expand_struct_name(&event); + let params = expand_params(&event).unwrap(); + let (definition, construction) = expand_data_struct(&name, ¶ms); + + assert_quote!(definition, { + struct Foo { + pub a: bool, + pub p1: self::ethcontract::Address, + } + }); + assert_quote!(construction, { Foo { a, p1 } }); + } + + #[test] + fn expand_data_tuple_value() { + let event = Event { + name: "Foo".into(), + inputs: vec![ + EventParam { + name: String::new(), + kind: ParamType::Bool, + indexed: false, + }, + EventParam { + name: String::new(), + kind: ParamType::Address, + indexed: false, + }, + ], + anonymous: false, + }; + + let name = expand_struct_name(&event); + let params = expand_params(&event).unwrap(); + let (definition, construction) = expand_data_tuple(&name, ¶ms); + + assert_quote!(definition, { + struct Foo(pub bool, pub self::ethcontract::Address); + }); + assert_quote!(construction, { Foo(p0, p1) }); + } + + // #[test] + // fn expand_enum_for_all_events() { + // let context = { + // let mut context = Context::default(); + // context.abi.events.insert( + // "Foo".into(), + // vec![Event { + // name: "Foo".into(), + // inputs: vec![EventParam { + // name: String::new(), + // kind: ParamType::Bool, + // indexed: false, + // }], + // anonymous: false, + // }], + // ); + // context.abi.events.insert( + // "Bar".into(), + // vec![Event { + // name: "Bar".into(), + // inputs: vec![EventParam { + // name: String::new(), + // kind: ParamType::Address, + // indexed: false, + // }], + // anonymous: true, + // }], + // ); + // context.event_derives = ["Asdf", "a::B", "a::b::c::D"] + // .iter() + // .map(|derive| syn::parse_str::(derive).unwrap()) + // .collect(); + // context + // }; + + // assert_quote!(expand_event_enum(&context), { + // /// A contract event. + // #[derive(Clone, Debug, Eq, PartialEq, Asdf, a::B, a::b::c::D)] + // pub enum Event { + // Bar(self::event_data::Bar), + // Foo(self::event_data::Foo), + // } + // }); + // } + + #[test] + // fn expand_parse_log_impl_for_all_events() { + // let context = { + // let mut context = Context::default(); + // context.abi.events.insert( + // "Foo".into(), + // vec![Event { + // name: "Foo".into(), + // inputs: vec![EventParam { + // name: String::new(), + // kind: ParamType::Bool, + // indexed: false, + // }], + // anonymous: false, + // }], + // ); + // context.abi.events.insert( + // "Bar".into(), + // vec![Event { + // name: "Bar".into(), + // inputs: vec![EventParam { + // name: String::new(), + // kind: ParamType::Address, + // indexed: false, + // }], + // anonymous: true, + // }], + // ); + // context + // }; + + // let foo_signature = expand_hash(context.abi.event("Foo").unwrap().signature()); + // let invalid_data = expand_invalid_data(); + + // assert_quote!(expand_event_parse_log(&context), { + // impl self::ethcontract::contract::ParseLog for Event { + // fn parse_log( + // log: self::ethcontract::RawLog, + // ) -> Result { + // let standard_event = log.topics + // .get(0) + // .copied() + // .map(|topic| match topic { + // #foo_signature => Ok(Event::Foo( + // log.clone().decode( + // &Contract::artifact() + // .abi + // .event("Foo") + // .expect("generated event decode") + // )? + // )), + // _ => #invalid_data, + // }); + + // if let Some(Ok(data)) = standard_event { + // return Ok(data); + // } + + // if let Ok(data) = log.clone().decode( + // &Contract::artifact() + // .abi + // .event("Bar") + // .expect("generated event decode") + // ) { + // return Ok(Event::Bar(data)); + // } + + // #invalid_data + // } + // } + // }); + // } + + #[test] + #[rustfmt::skip] + fn expand_hash_value() { + assert_quote!( + expand_hash( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".parse().unwrap() + ), + { + self::ethcontract::H256([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + ]) + }, + ); + } +} diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs new file mode 100644 index 00000000..7e05c4be --- /dev/null +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -0,0 +1,168 @@ +use super::{types, util, Context}; + +use ethers_abi::{Function, FunctionExt, Param}; +use ethers_types::Selector; + +use anyhow::{anyhow, Context as _, Result}; +use inflector::Inflector; +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use syn::Ident; + +/// Expands a context into a method struct containing all the generated bindings +/// to the Solidity contract methods. +impl Context { + pub(crate) fn methods(&self) -> Result { + let mut aliases = self.method_aliases.clone(); + + let functions = self + .abi + .functions() + .map(|function| { + let signature = function.abi_signature(); + expand_function(function, aliases.remove(&signature)) + .with_context(|| format!("error expanding function '{}'", signature)) + }) + .collect::>>()?; + + Ok(quote! { #( #functions )* }) + } +} + +#[allow(unused)] +fn expand_function(function: &Function, alias: Option) -> Result { + let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); + let selector = expand_selector(function.selector()); + + let input = expand_inputs(&function.inputs)?; + + let outputs = expand_fn_outputs(&function.outputs)?; + + let result = if function.constant { + quote! { Sender<'a, S, P, #outputs> } + } else { + quote! { Sender<'a, S, P, H256> } + }; + + let arg = expand_inputs_call_arg(&function.inputs); + let doc = util::expand_doc(&format!("Calls the contract's {} function", function.name)); + Ok(quote! { + + #doc + pub fn #name(&self #input) -> #result { + self.0.method_hash(#selector, #arg) + .expect("method not found (this should never happen)") + } + }) +} + +// converts the function params to name/type pairs +pub(crate) fn expand_inputs(inputs: &[Param]) -> Result { + let params = inputs + .iter() + .enumerate() + .map(|(i, param)| { + let name = util::expand_input_name(i, ¶m.name); + let kind = types::expand(¶m.kind)?; + Ok(quote! { #name: #kind }) + }) + .collect::>>()?; + Ok(quote! { #( , #params )* }) +} + +// packs the argument in a tuple to be used for the contract call +pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { + let names = inputs + .iter() + .enumerate() + .map(|(i, param)| util::expand_input_name(i, ¶m.name)); + quote! { ( #( #names ,)* ) } +} + +fn expand_fn_outputs(outputs: &[Param]) -> Result { + match outputs.len() { + 0 => Ok(quote! { () }), + 1 => types::expand(&outputs[0].kind), + _ => { + let types = outputs + .iter() + .map(|param| types::expand(¶m.kind)) + .collect::>>()?; + Ok(quote! { (#( #types ),*) }) + } + } +} + +fn expand_selector(selector: Selector) -> TokenStream { + let bytes = selector.iter().copied().map(Literal::u8_unsuffixed); + quote! { [#( #bytes ),*] } +} + +#[cfg(test)] +mod tests { + use super::*; + use ethers_abi::ParamType; + + #[test] + fn expand_inputs_empty() { + assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},); + } + + #[test] + fn expand_inputs_() { + assert_quote!( + expand_inputs( + + &[ + Param { + name: "a".to_string(), + kind: ParamType::Bool, + }, + Param { + name: "b".to_string(), + kind: ParamType::Address, + }, + ], + ) + .unwrap(), + { , a: bool, b: self::ethcontract::Address }, + ); + } + + #[test] + fn expand_fn_outputs_empty() { + assert_quote!(expand_fn_outputs(&[],).unwrap(), { + self::ethcontract::Void + }); + } + + #[test] + fn expand_fn_outputs_single() { + assert_quote!( + expand_fn_outputs(&[Param { + name: "a".to_string(), + kind: ParamType::Bool, + }]) + .unwrap(), + { bool }, + ); + } + + #[test] + fn expand_fn_outputs_muliple() { + assert_quote!( + expand_fn_outputs(&[ + Param { + name: "a".to_string(), + kind: ParamType::Bool, + }, + Param { + name: "b".to_string(), + kind: ParamType::Address, + }, + ],) + .unwrap(), + { (bool, self::ethcontract::Address) }, + ); + } +} diff --git a/crates/ethers-derive/src/src/contract/types.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/types.rs similarity index 87% rename from crates/ethers-derive/src/src/contract/types.rs rename to crates/ethers-contract/ethers-contract-abigen/src/contract/types.rs index 176e3970..eba64c02 100644 --- a/crates/ethers-derive/src/src/contract/types.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/types.rs @@ -1,11 +1,11 @@ use anyhow::{anyhow, Result}; -use ethcontract_common::abi::ParamType; +use ethers_abi::ParamType; use proc_macro2::{Literal, TokenStream}; use quote::quote; pub(crate) fn expand(kind: &ParamType) -> Result { match kind { - ParamType::Address => Ok(quote! { self::ethcontract::Address }), + ParamType::Address => Ok(quote! { Address }), ParamType::Bytes => Ok(quote! { Vec }), ParamType::Int(n) => match n / 8 { 1 => Ok(quote! { i8 }), @@ -13,7 +13,7 @@ pub(crate) fn expand(kind: &ParamType) -> Result { 3..=4 => Ok(quote! { i32 }), 5..=8 => Ok(quote! { i64 }), 9..=16 => Ok(quote! { i128 }), - 17..=32 => Ok(quote! { self::ethcontract::I256 }), + 17..=32 => Ok(quote! { I256 }), _ => Err(anyhow!("unsupported solidity type int{}", n)), }, ParamType::Uint(n) => match n / 8 { @@ -22,7 +22,7 @@ pub(crate) fn expand(kind: &ParamType) -> Result { 3..=4 => Ok(quote! { u32 }), 5..=8 => Ok(quote! { u64 }), 9..=16 => Ok(quote! { u128 }), - 17..=32 => Ok(quote! { self::ethcontract::U256 }), + 17..=32 => Ok(quote! { U256 }), _ => Err(anyhow!("unsupported solidity type uint{}", n)), }, ParamType::Bool => Ok(quote! { bool }), @@ -43,6 +43,7 @@ pub(crate) fn expand(kind: &ParamType) -> Result { let size = Literal::usize_unsuffixed(*n); Ok(quote! { [#inner; #size] }) } + // TODO: Implement abiencoder v2 ParamType::Tuple(_) => Err(anyhow!("ABIEncoderV2 is currently not supported")), } } diff --git a/crates/ethers-derive/src/src/lib.rs b/crates/ethers-contract/ethers-contract-abigen/src/lib.rs similarity index 63% rename from crates/ethers-derive/src/src/lib.rs rename to crates/ethers-contract/ethers-contract-abigen/src/lib.rs index 8a83289c..d823434e 100644 --- a/crates/ethers-derive/src/src/lib.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -1,8 +1,10 @@ +#![allow(dead_code)] +#![allow(unused_imports)] #![deny(missing_docs, unsafe_code)] -//! Crate for generating type-safe bindings to Ethereum smart contracts. This -//! crate is intended to be used either indirectly with the `ethcontract` -//! crate's `contract` procedural macro or directly from a build script. +//! Module for generating type-safe bindings to Ethereum smart contracts. This +//! module is intended to be used either indirectly with the `abigen` procedural +//! macro or directly from a build script / CLI #[cfg(test)] #[allow(missing_docs)] @@ -11,14 +13,17 @@ mod test_macros; mod contract; +use contract::Context; + mod rustfmt; mod source; mod util; -pub use crate::source::Source; -pub use crate::util::parse_address; +pub use ethers_types::Address; +pub use source::Source; +pub use util::parse_address; + use anyhow::Result; -pub use ethcontract_common::Address; use proc_macro2::TokenStream; use std::collections::HashMap; use std::fs::File; @@ -28,22 +33,26 @@ use std::path::Path; /// Internal global arguments passed to the generators for each individual /// component that control expansion. pub(crate) struct Args { - /// The source of the truffle artifact JSON for the contract whose bindings + /// The source of the ABI JSON for the contract whose bindings /// are being generated. - artifact_source: Source, + abi_source: Source, + + /// Override the contract name to use for the generated type. + contract_name: String, + /// The runtime crate name to use. runtime_crate_name: String, + /// The visibility modifier to use for the generated module and contract /// re-export. visibility_modifier: Option, + /// Override the contract module name that contains the generated code. contract_mod_override: Option, - /// Override the contract name to use for the generated type. - contract_name_override: Option, - /// Manually specified deployed contract addresses. - deployments: HashMap, + /// Manually specified contract method aliases. method_aliases: HashMap, + /// Derives added to event structs and enums. event_derives: Vec, } @@ -51,14 +60,14 @@ pub(crate) struct Args { impl Args { /// Creates a new builder given the path to a contract's truffle artifact /// JSON file. - pub fn new(source: Source) -> Self { + pub fn new(contract_name: &str, abi_source: Source) -> Self { Args { - artifact_source: source, - runtime_crate_name: "ethcontract".to_owned(), + abi_source, + contract_name: contract_name.to_owned(), + + runtime_crate_name: "abigen".to_owned(), visibility_modifier: None, contract_mod_override: None, - contract_name_override: None, - deployments: HashMap::new(), method_aliases: HashMap::new(), event_derives: Vec::new(), } @@ -88,35 +97,39 @@ pub struct Builder { } impl Builder { - /// Creates a new builder given the path to a contract's truffle artifact - /// JSON file. - pub fn new

(artifact_path: P) -> Self + /// Creates a new builder given the contract's ABI JSON string + pub fn from_str(name: &str, abi: &str) -> Self { + Builder::source(name, Source::String(abi.to_owned())) + } + + /// Creates a new builder given the path to a contract's ABI file + pub fn new

(name: &str, path: P) -> Self where P: AsRef, { - Builder::with_source(Source::local(artifact_path)) + Builder::source(name, Source::local(path)) } /// Creates a new builder from a source URL. - pub fn from_source_url(source_url: S) -> Result + pub fn from_url(name: &str, url: S) -> Result where S: AsRef, { - let source = Source::parse(source_url)?; - Ok(Builder::with_source(source)) + let source = Source::parse(url)?; + Ok(Builder::source(name, source)) } - /// Creates a new builder with the given artifact JSON source. - pub fn with_source(source: Source) -> Self { + /// Creates a new builder with the given ABI JSON source. + pub fn source(name: &str, source: Source) -> Self { Builder { - args: Args::new(source), + args: Args::new(name, source), options: SerializationOptions::default(), } } /// Sets the crate name for the runtime crate. This setting is usually only /// needed if the crate was renamed in the Cargo manifest. - pub fn with_runtime_crate_name(mut self, name: S) -> Self + pub fn runtime_crate_name(mut self, name: S) -> Self where S: Into, { @@ -126,7 +139,7 @@ impl Builder { /// Sets an optional visibility modifier for the generated module and /// contract re-export. - pub fn with_visibility_modifier(mut self, vis: Option) -> Self + pub fn visibility_modifier(mut self, vis: Option) -> Self where S: Into, { @@ -135,7 +148,7 @@ impl Builder { } /// Sets the optional contract module name override. - pub fn with_contract_mod_override(mut self, name: Option) -> Self + pub fn contract_mod_override(mut self, name: Option) -> Self where S: Into, { @@ -143,48 +156,6 @@ impl Builder { self } - /// Sets the optional contract name override. This setting is needed when - /// using a artifact JSON source that does not provide a contract name such - /// as Etherscan. - pub fn with_contract_name_override(mut self, name: Option) -> Self - where - S: Into, - { - self.args.contract_name_override = name.map(S::into); - self - } - - /// Manually adds specifies the deployed address of a contract for a given - /// network. Note that manually specified deployments take precedence over - /// deployments in the Truffle artifact (in the `networks` property of the - /// artifact). - /// - /// This is useful for integration test scenarios where the address of a - /// contract on the test node is deterministic (for example using - /// `ganache-cli -d`) but the contract address is not part of the Truffle - /// artifact; or to override a deployment included in a Truffle artifact. - pub fn add_deployment(mut self, network_id: u32, address: Address) -> Self { - self.args.deployments.insert(network_id, address); - self - } - - /// Manually adds specifies the deployed address as a string of a contract - /// for a given network. See `Builder::add_deployment` for more information. - /// - /// # Panics - /// - /// This method panics if the specified address string is invalid. See - /// `parse_address` for more information on the address string format. - pub fn add_deployment_str(self, network_id: u32, address: S) -> Self - where - S: AsRef, - { - self.add_deployment( - network_id, - parse_address(address).expect("failed to parse address"), - ) - } - /// Manually adds a solidity method alias to specify what the method name /// will be in Rust. For solidity methods without an alias, the snake cased /// method name will be used. @@ -204,7 +175,7 @@ impl Builder { /// /// Note that in case `rustfmt` does not exist or produces an error, the /// unformatted code will be used. - pub fn with_rustfmt(mut self, rustfmt: bool) -> Self { + pub fn rustfmt(mut self, rustfmt: bool) -> Self { self.options.rustfmt = rustfmt; self } @@ -232,7 +203,7 @@ impl Builder { /// Generates the contract bindings. pub fn generate(self) -> Result { - let tokens = contract::expand(self.args)?; + let tokens = Context::expand(self.args)?; Ok(ContractBindings { tokens, options: self.options, diff --git a/crates/ethers-derive/src/src/rustfmt.rs b/crates/ethers-contract/ethers-contract-abigen/src/rustfmt.rs similarity index 100% rename from crates/ethers-derive/src/src/rustfmt.rs rename to crates/ethers-contract/ethers-contract-abigen/src/rustfmt.rs diff --git a/crates/ethers-derive/src/src/source.rs b/crates/ethers-contract/ethers-contract-abigen/src/source.rs similarity index 90% rename from crates/ethers-derive/src/src/source.rs rename to crates/ethers-contract/ethers-contract-abigen/src/source.rs index 335288f2..03cc3221 100644 --- a/crates/ethers-derive/src/src/source.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/source.rs @@ -1,48 +1,54 @@ //! Module implements reading of contract artifacts from various sources. +use super::util; +use ethers_types::Address; -use crate::util; use anyhow::{anyhow, Context, Error, Result}; -use ethcontract_common::Address; -use std::borrow::Cow; -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::str::FromStr; +use std::{ + borrow::Cow, + env, fs, + path::{Path, PathBuf}, + str::FromStr, +}; use url::Url; /// A source of a Truffle artifact JSON. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Source { - /// A Truffle artifact or ABI located on the local file system. + /// A raw ABI string + String(String), + + /// An ABI located on the local file system. Local(PathBuf), - /// A truffle artifact or ABI to be retrieved over HTTP(S). + + /// An ABI to be retrieved over HTTP(S). Http(Url), + /// An address of a mainnet contract that has been verified on Etherscan.io. Etherscan(Address), + /// The package identifier of an npm package with a path to a Truffle /// artifact or ABI to be retrieved from `unpkg.io`. Npm(String), } impl Source { - /// Parses an artifact source from a string. + /// Parses an ABI from a source /// - /// Contract artifacts can be retrieved from the local filesystem or online - /// from `etherscan.io`, this method parses artifact source URLs and accepts + /// Contract ABIs can be retrieved from the local filesystem or online + /// from `etherscan.io`, this method parses ABI source URLs and accepts /// the following: - /// - `relative/path/to/Contract.json`: a relative path to a truffle - /// artifact JSON file. This relative path is rooted in the current - /// working directory. To specify the root for relative paths, use - /// `Source::with_root`. + /// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file. + /// This relative path is rooted in the current working directory. + /// To specify the root for relative paths, use `Source::with_root`. /// - `/absolute/path/to/Contract.json` or /// `file:///absolute/path/to/Contract.json`: an absolute path or file URL - /// to a truffle artifact JSON file. - /// - `http(s)://...` an HTTP url to a contract ABI or Truffle artifact. + /// to an ABI JSON file. + /// - `http(s)://...` an HTTP url to a contract ABI. /// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a /// address or URL of a verified contract on Etherscan. /// - `npm:@org/package@1.0.0/path/to/contract.json` an npmjs package with /// an optional version and path (defaulting to the latest version and - /// `index.js`). The contract artifact or ABI will be retrieved through + /// `index.js`). The contract ABI will be retrieved through /// `unpkg.io`. pub fn parse(source: S) -> Result where @@ -118,12 +124,13 @@ impl Source { /// Retrieves the source JSON of the artifact this will either read the JSON /// from the file system or retrieve a contract ABI from the network /// dependending on the source type. - pub fn artifact_json(&self) -> Result { + pub fn get(&self) -> Result { match self { Source::Local(path) => get_local_contract(path), Source::Http(url) => get_http_contract(url), Source::Etherscan(address) => get_etherscan_contract(*address), Source::Npm(package) => get_npm_contract(package), + Source::String(abi) => Ok(abi.clone()), } } } diff --git a/crates/ethers-derive/src/src/test/macros.rs b/crates/ethers-contract/ethers-contract-abigen/src/test/macros.rs similarity index 100% rename from crates/ethers-derive/src/src/test/macros.rs rename to crates/ethers-contract/ethers-contract-abigen/src/test/macros.rs diff --git a/crates/ethers-derive/src/src/util.rs b/crates/ethers-contract/ethers-contract-abigen/src/util.rs similarity index 98% rename from crates/ethers-derive/src/src/util.rs rename to crates/ethers-contract/ethers-contract-abigen/src/util.rs index d3632cbe..4ef8adef 100644 --- a/crates/ethers-derive/src/src/util.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/util.rs @@ -1,6 +1,7 @@ +use ethers_types::Address; + use anyhow::{anyhow, Result}; use curl::easy::Easy; -use ethcontract_common::Address; use inflector::Inflector; use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::quote; diff --git a/crates/ethers-contract/ethers-contract-abigen/token.rs b/crates/ethers-contract/ethers-contract-abigen/token.rs new file mode 100644 index 00000000..f443bec7 --- /dev/null +++ b/crates/ethers-contract/ethers-contract-abigen/token.rs @@ -0,0 +1,185 @@ +pub use erc20token_mod::ERC20Token; +mod erc20token_mod { + use ethers_contract::{ + abi::{Abi, Detokenize, InvalidOutputType, Token, Tokenizable}, + providers::JsonRpcClient, + signers::{Client, Signer}, + types::*, + Contract, Lazy, + }; + static ABI: Lazy = Lazy::new(|| { + serde_json :: from_str ( "[{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"name\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"name\":\"symbol\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"name\":\"decimals\",\"type\":\"uint8\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"totalSupply\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"who\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"balance\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"remaining\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"}]" ) . expect ( "invalid abi" ) + }); + #[derive(Clone)] + pub struct ERC20Token<'a, S, P>(Contract<'a, S, P>); + impl<'a, S, P> std::ops::Deref for ERC20Token<'a, S, P> { + type Target = Contract<'a, S, P>; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl<'a, S: Signer, P: JsonRpcClient> std::fmt::Debug for ERC20Token<'a, S, P> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple(stringify!(ERC20Token)) + .field(&self.address()) + .finish() + } + } + impl<'a, S: Signer, P: JsonRpcClient> ERC20Token<'a, S, P> { + #[doc = r" Creates a new contract instance with the specified `ethers`"] + #[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"] + #[doc = r" object"] + pub fn new>(address: T, client: &'a Client<'a, S, P>) -> Self { + let contract = Contract::new(client, ABI.clone(), address.into()); + Self(contract) + } + #[doc = "Calls the contract's balanceOf function"] + pub fn balance_of(&self, who: Address) -> Sender<'a, S, P, U256> { + self.0 + .method([112, 160, 130, 49], (who,)) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's transfer function"] + pub fn transfer(&self, to: Address, value: U256) -> Sender<'a, S, P, H256> { + self.0 + .method([169, 5, 156, 187], (to, value)) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's approve function"] + pub fn approve(&self, spender: Address, value: U256) -> Sender<'a, S, P, H256> { + self.0 + .method([9, 94, 167, 179], (spender, value)) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's name function"] + pub fn name(&self) -> Sender<'a, S, P, String> { + self.0 + .method([6, 253, 222, 3], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's decimals function"] + pub fn decimals(&self) -> Sender<'a, S, P, u8> { + self.0 + .method([49, 60, 229, 103], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's symbol function"] + pub fn symbol(&self) -> Sender<'a, S, P, String> { + self.0 + .method([149, 216, 155, 65], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's totalSupply function"] + pub fn total_supply(&self) -> Sender<'a, S, P, U256> { + self.0 + .method([24, 22, 13, 221], ()) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's transferFrom function"] + pub fn transfer_from( + &self, + from: Address, + to: Address, + value: U256, + ) -> Sender<'a, S, P, H256> { + self.0 + .method([35, 184, 114, 221], (from, to, value)) + .expect("method not found (this should never happen)") + } + #[doc = "Calls the contract's allowance function"] + pub fn allowance(&self, owner: Address, spender: Address) -> Sender<'a, S, P, U256> { + self.0 + .method([221, 98, 237, 62], (owner, spender)) + .expect("method not found (this should never happen)") + } + } + #[derive(Clone, Debug, Default, Eq, PartialEq)] + pub struct Transfer { + pub from: Address, + pub to: Address, + pub value: U256, + } + impl Transfer { + #[doc = r" Retrieves the signature for the event this data corresponds to."] + #[doc = r" This signature is the Keccak-256 hash of the ABI signature of"] + #[doc = r" this event."] + pub const fn signature() -> H256 { + H256([ + 221, 242, 82, 173, 27, 226, 200, 155, 105, 194, 176, 104, 252, 55, 141, 170, 149, + 43, 167, 241, 99, 196, 161, 22, 40, 245, 90, 77, 245, 35, 179, 239, + ]) + } + #[doc = r" Retrieves the ABI signature for the event this data corresponds"] + #[doc = r" to. For this event the value should always be:"] + #[doc = r""] + #[doc = "`Transfer(address,address,uint256)`"] + pub const fn abi_signature() -> &'static str { + "Transfer(address,address,uint256)" + } + } + impl Detokenize for Transfer { + fn from_tokens(tokens: Vec) -> Result { + if tokens.len() != 3 { + return Err(InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + 3, + tokens.len(), + tokens + ))); + } + #[allow(unused_mut)] + let mut tokens = tokens.into_iter(); + let from = Address::from_token(tokens.next().expect("this should never happen"))?; + let to = Address::from_token(tokens.next().expect("this should never happen"))?; + let value = U256::from_token(tokens.next().expect("this should never happen"))?; + Ok(Transfer { from, to, value }) + } + } + #[derive(Clone, Debug, Default, Eq, PartialEq)] + pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256, + } + impl Approval { + #[doc = r" Retrieves the signature for the event this data corresponds to."] + #[doc = r" This signature is the Keccak-256 hash of the ABI signature of"] + #[doc = r" this event."] + pub const fn signature() -> H256 { + H256([ + 140, 91, 225, 229, 235, 236, 125, 91, 209, 79, 113, 66, 125, 30, 132, 243, 221, 3, + 20, 192, 247, 178, 41, 30, 91, 32, 10, 200, 199, 195, 185, 37, + ]) + } + #[doc = r" Retrieves the ABI signature for the event this data corresponds"] + #[doc = r" to. For this event the value should always be:"] + #[doc = r""] + #[doc = "`Approval(address,address,uint256)`"] + pub const fn abi_signature() -> &'static str { + "Approval(address,address,uint256)" + } + } + impl Detokenize for Approval { + fn from_tokens(tokens: Vec) -> Result { + if tokens.len() != 3 { + return Err(InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + 3, + tokens.len(), + tokens + ))); + } + #[allow(unused_mut)] + let mut tokens = tokens.into_iter(); + let owner = Address::from_token(tokens.next().expect("this should never happen"))?; + let spender = Address::from_token(tokens.next().expect("this should never happen"))?; + let value = U256::from_token(tokens.next().expect("this should never happen"))?; + Ok(Approval { + owner, + spender, + value, + }) + } + } +} +fn main() {} diff --git a/crates/ethers-contract/ethers-contract-derive/Cargo.toml b/crates/ethers-contract/ethers-contract-derive/Cargo.toml new file mode 100644 index 00000000..31d8540e --- /dev/null +++ b/crates/ethers-contract/ethers-contract-derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ethers-contract-derive" +version = "0.1.0" +authors = ["Nicholas Rodrigues Lordello ", "Georgios Konstantopoulos "] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "Proc macro for type-safe bindings generation to Ethereum smart contracts" + +[lib] +proc-macro = true + +[dependencies] +ethers-abi = { path = "../../ethers-abi" } +ethers-contract-abigen = { path = "../ethers-contract-abigen" } + +serde_json = "1.0.53" + +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0.12" diff --git a/crates/ethers-derive/src/lib.rs b/crates/ethers-contract/ethers-contract-derive/src/abigen.rs similarity index 59% rename from crates/ethers-derive/src/lib.rs rename to crates/ethers-contract/ethers-contract-derive/src/abigen.rs index b0e00814..901e39eb 100644 --- a/crates/ethers-derive/src/lib.rs +++ b/crates/ethers-contract/ethers-contract-derive/src/abigen.rs @@ -1,132 +1,40 @@ //! Implementation of procedural macro for generating type-safe bindings to an //! ethereum smart contract. -#![deny(missing_docs, unsafe_code)] +use crate::spanned::{ParseInner, Spanned}; -extern crate proc_macro; +use ethers_abi::{Function, FunctionExt, Param}; +use ethers_contract_abigen::Builder; -mod spanned; -use crate::spanned::{ParseInner, Spanned, parse_address, Address, Builder}; - -use ethers::abi::{Function, Param, ParamType, FunctionExt, ParamTypeExt}; - -use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens as _}; use std::collections::HashSet; use std::error::Error; use syn::ext::IdentExt; use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult}; -use syn::{ - braced, parenthesized, parse_macro_input, Error as SynError, Ident, LitInt, LitStr, Path, - Token, Visibility, -}; +use syn::{braced, parenthesized, Ident, LitStr, Path, Token, Visibility}; -// TODO: Make it accept an inline ABI array -/// Proc macro to generate type-safe bindings to a contract. This macro accepts -/// an Ethereum contract ABIABI or a path. Note that this path is rooted in -/// the crate's root `CARGO_MANIFEST_DIR`. -/// -/// ```ignore -/// ethcontract::contract!("build/contracts/MyContract.json"); -/// ``` -/// -/// Alternatively, other sources may be used, for full details consult the -/// `ethcontract-generate::source` documentation. Some basic examples: -/// -/// ```ignore -/// // HTTP(S) source -/// ethcontract::contract!("https://my.domain.local/path/to/contract.json") -/// // Etherscan.io -/// ethcontract::contract!("etherscan:0x0001020304050607080910111213141516171819"); -/// ethcontract::contract!("https://etherscan.io/address/0x0001020304050607080910111213141516171819"); -/// // npmjs -/// ethcontract::contract!("npm:@org/package@1.0.0/path/to/contract.json") -/// ``` -/// -/// Note that Etherscan rate-limits requests to their API, to avoid this an -/// `ETHERSCAN_API_KEY` environment variable can be set. If it is, it will use -/// that API key when retrieving the contract ABI. -/// -/// Currently the proc macro accepts additional parameters to configure some -/// aspects of the code generation. Specifically it accepts: -/// - `crate`: The name of the `ethcontract` crate. This is useful if the crate -/// was renamed in the `Cargo.toml` for whatever reason. -/// - `contract`: Override the contract name that is used for the generated -/// type. This is required when using sources that do not provide the contract -/// name in the artifact JSON such as Etherscan. -/// - `mod`: The name of the contract module to place generated code in. Note -/// that the root contract type gets re-exported in the context where the -/// macro was invoked. This defaults to the contract name converted into snake -/// case. -/// - `methods`: A list of mappings from method signatures to method names -/// allowing methods names to be explicitely set for contract methods. This -/// also provides a workaround for generating code for contracts with multiple -/// methods with the same name. -/// - `event_derives`: A list of additional derives that should be added to -/// contract event structs and enums. -/// -/// Additionally, the ABI source can be preceeded by a visibility modifier such -/// as `pub` or `pub(crate)`. This visibility modifier is applied to both the -/// generated module and contract re-export. If no visibility modifier is -/// provided, then none is used for the generated code as well, making the -/// module and contract private to the scope where the macro was invoked. -/// -/// ```ignore -/// ethcontract::contract!( -/// pub(crate) "build/contracts/MyContract.json", -/// crate = ethcontract_rename, -/// mod = my_contract_instance, -/// contract = MyContractInstance, -/// deployments { -/// 4 => "0x000102030405060708090a0b0c0d0e0f10111213", -/// 5777 => "0x0123456789012345678901234567890123456789", -/// }, -/// methods { -/// myMethod(uint256,bool) as my_renamed_method; -/// }, -/// event_derives (serde::Deserialize, serde::Serialize), -/// ); -/// ``` -/// -/// See [`ethcontract`](ethcontract) module level documentation for additional -/// information. -#[proc_macro] -pub fn contract(input: TokenStream) -> TokenStream { - let args = parse_macro_input!(input as Spanned); - - let span = args.span(); - expand(args.into_inner()) - .unwrap_or_else(|e| SynError::new(span, format!("{:?}", e)).to_compile_error()) - .into() -} - -fn expand(args: ContractArgs) -> Result> { +pub(crate) fn expand(args: ContractArgs) -> Result> { Ok(args.into_builder()?.generate()?.into_tokens()) } /// Contract procedural macro arguments. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] -struct ContractArgs { +pub(crate) struct ContractArgs { visibility: Option, - artifact_path: String, + name: String, + path: String, parameters: Vec, } impl ContractArgs { fn into_builder(self) -> Result> { - let mut builder = Builder::from_source_url(&self.artifact_path)? - .with_visibility_modifier(self.visibility); + let mut builder = + Builder::from_str(&self.name, &self.path).visibility_modifier(self.visibility); for parameter in self.parameters.into_iter() { builder = match parameter { - Parameter::Mod(name) => builder.with_contract_mod_override(Some(name)), - Parameter::Contract(name) => builder.with_contract_name_override(Some(name)), - Parameter::Crate(name) => builder.with_runtime_crate_name(name), - Parameter::Deployments(deployments) => { - deployments.into_iter().fold(builder, |builder, d| { - builder.add_deployment(d.network_id, d.address) - }) - } + Parameter::Mod(name) => builder.contract_mod_override(Some(name)), + Parameter::Crate(name) => builder.runtime_crate_name(name), Parameter::Methods(methods) => methods.into_iter().fold(builder, |builder, m| { builder.add_method_alias(m.signature, m.alias) }), @@ -142,17 +50,24 @@ impl ContractArgs { impl ParseInner for ContractArgs { fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> { + // read the visibility parameter let visibility = match input.parse::()? { Visibility::Inherited => None, token => Some(quote!(#token).to_string()), }; + // read the contract name + let name = input.parse::()?.to_string(); + + // skip the comma + input.parse::()?; + // TODO(nlordell): Due to limitation with the proc-macro Span API, we // can't currently get a path the the file where we were called from; // therefore, the path will always be rooted on the cargo manifest // directory. Eventually we can use the `Span::source_file` API to // have a better experience. - let (span, artifact_path) = { + let (span, path) = { let literal = input.parse::()?; (literal.span(), literal.value()) }; @@ -160,6 +75,7 @@ impl ParseInner for ContractArgs { if !input.is_empty() { input.parse::()?; } + let parameters = input .parse_terminated::<_, Token![,]>(Parameter::parse)? .into_iter() @@ -169,7 +85,8 @@ impl ParseInner for ContractArgs { span, ContractArgs { visibility, - artifact_path, + name, + path, parameters, }, )) @@ -180,9 +97,7 @@ impl ParseInner for ContractArgs { #[cfg_attr(test, derive(Debug, Eq, PartialEq))] enum Parameter { Mod(String), - Contract(String), Crate(String), - Deployments(Vec), Methods(Vec), EventDerives(Vec), } @@ -201,35 +116,6 @@ impl Parse for Parameter { let name = input.parse::()?.to_string(); Parameter::Mod(name) } - "contract" => { - input.parse::()?; - let name = input.parse::()?.to_string(); - Parameter::Contract(name) - } - "deployments" => { - let content; - braced!(content in input); - let deployments = { - let parsed = - content.parse_terminated::<_, Token![,]>(Spanned::::parse)?; - - let mut deployments = Vec::with_capacity(parsed.len()); - let mut networks = HashSet::new(); - for deployment in parsed { - if !networks.insert(deployment.network_id) { - return Err(ParseError::new( - deployment.span(), - "duplicate network ID in `ethcontract::contract!` macro invocation", - )); - } - deployments.push(deployment.into_inner()) - } - - deployments - }; - - Parameter::Deployments(deployments) - } "methods" => { let content; braced!(content in input); @@ -283,29 +169,6 @@ impl Parse for Parameter { } } -/// A manually specified dependency. -#[cfg_attr(test, derive(Debug, Eq, PartialEq))] -struct Deployment { - network_id: u32, - address: Address, -} - -impl Parse for Deployment { - fn parse(input: ParseStream) -> ParseResult { - let network_id = input.parse::()?.base10_parse()?; - input.parse::]>()?; - let address = { - let literal = input.parse::()?; - parse_address(&literal.value()).map_err(|err| ParseError::new(literal.span(), err))? - }; - - Ok(Deployment { - network_id, - address, - }) - } -} - /// An explicitely named contract method. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] struct Method { @@ -324,7 +187,7 @@ impl Parse for Method { .parse_terminated::<_, Token![,]>(Ident::parse)? .iter() .map(|ident| { - let kind = ParamType::from_str(&ident.to_string()) + let kind = serde_json::from_value(serde_json::json!(&ident.to_string())) .map_err(|err| ParseError::new(ident.span(), err))?; Ok(Param { name: "".into(), @@ -425,10 +288,6 @@ mod tests { crate = foobar, mod = contract, contract = Contract, - deployments { - 1 => "0x000102030405060708090a0b0c0d0e0f10111213", - 4 => "0x0123456789012345678901234567890123456789", - }, methods { myMethod(uint256, bool) as my_renamed_method; myOtherMethod() as my_other_renamed_method; @@ -462,17 +321,6 @@ mod tests { ); } - #[test] - fn duplicate_network_id_error() { - contract_args_err!( - "artifact.json", - deployments { - 1 => "0x000102030405060708090a0b0c0d0e0f10111213", - 1 => "0x0123456789012345678901234567890123456789", - } - ); - } - #[test] fn duplicate_method_rename_error() { contract_args_err!( diff --git a/crates/ethers-contract/ethers-contract-derive/src/lib.rs b/crates/ethers-contract/ethers-contract-derive/src/lib.rs new file mode 100644 index 00000000..0ef79630 --- /dev/null +++ b/crates/ethers-contract/ethers-contract-derive/src/lib.rs @@ -0,0 +1,86 @@ +//! Implementation of procedural macro for generating type-safe bindings to an +//! ethereum smart contract. +#![deny(missing_docs, unsafe_code)] + +mod spanned; +use spanned::Spanned; + +mod abigen; +use abigen::{expand, ContractArgs}; + +use proc_macro::TokenStream; +use syn::{parse::Error, parse_macro_input}; + +/// Proc macro to generate type-safe bindings to a contract. This macro accepts +/// an Ethereum contract ABI or a path. Note that this path is rooted in +/// the crate's root `CARGO_MANIFEST_DIR`. +/// +/// ```ignore +/// ethcontract::contract!("build/contracts/MyContract.json"); +/// ``` +/// +/// Alternatively, other sources may be used, for full details consult the +/// `ethcontract-generate::source` documentation. Some basic examples: +/// +/// ```ignore +/// // HTTP(S) source +/// ethcontract::contract!("https://my.domain.local/path/to/contract.json") +/// // Etherscan.io +/// ethcontract::contract!("etherscan:0x0001020304050607080910111213141516171819"); +/// ethcontract::contract!("https://etherscan.io/address/0x0001020304050607080910111213141516171819"); +/// // npmjs +/// ethcontract::contract!("npm:@org/package@1.0.0/path/to/contract.json") +/// ``` +/// +/// Note that Etherscan rate-limits requests to their API, to avoid this an +/// `ETHERSCAN_API_KEY` environment variable can be set. If it is, it will use +/// that API key when retrieving the contract ABI. +/// +/// Currently the proc macro accepts additional parameters to configure some +/// aspects of the code generation. Specifically it accepts: +/// - `crate`: The name of the `ethcontract` crate. This is useful if the crate +/// was renamed in the `Cargo.toml` for whatever reason. +/// - `contract`: Override the contract name that is used for the generated +/// type. This is required when using sources that do not provide the contract +/// name in the artifact JSON such as Etherscan. +/// - `mod`: The name of the contract module to place generated code in. Note +/// that the root contract type gets re-exported in the context where the +/// macro was invoked. This defaults to the contract name converted into snake +/// case. +/// - `methods`: A list of mappings from method signatures to method names +/// allowing methods names to be explicitely set for contract methods. This +/// also provides a workaround for generating code for contracts with multiple +/// methods with the same name. +/// - `event_derives`: A list of additional derives that should be added to +/// contract event structs and enums. +/// +/// Additionally, the ABI source can be preceeded by a visibility modifier such +/// as `pub` or `pub(crate)`. This visibility modifier is applied to both the +/// generated module and contract re-export. If no visibility modifier is +/// provided, then none is used for the generated code as well, making the +/// module and contract private to the scope where the macro was invoked. +/// +/// ```ignore +/// ethcontract::contract!( +/// pub(crate) "build/contracts/MyContract.json", +/// crate = ethcontract_rename, +/// mod = my_contract_instance, +/// contract = MyContractInstance, +/// methods { +/// myMethod(uint256,bool) as my_renamed_method; +/// }, +/// event_derives (serde::Deserialize, serde::Serialize), +/// ); +/// ``` +/// +/// See [`ethcontract`](ethcontract) module level documentation for additional +/// information. +#[proc_macro] +pub fn abigen(input: TokenStream) -> TokenStream { + let args = parse_macro_input!(input as Spanned); + + let span = args.span(); + expand(args.into_inner()) + .unwrap_or_else(|e| Error::new(span, format!("{:?}", e)).to_compile_error()) + .into() +} diff --git a/crates/ethers-derive/src/spanned.rs b/crates/ethers-contract/ethers-contract-derive/src/spanned.rs similarity index 100% rename from crates/ethers-derive/src/spanned.rs rename to crates/ethers-contract/ethers-contract-derive/src/spanned.rs diff --git a/crates/ethers-contract/src/contract.rs b/crates/ethers-contract/src/contract.rs new file mode 100644 index 00000000..9d0fd0f8 --- /dev/null +++ b/crates/ethers-contract/src/contract.rs @@ -0,0 +1,279 @@ +use ethers_abi::{ + Abi, Detokenize, Error, Event as AbiEvent, EventExt, Function, FunctionExt, RawLog, Tokenize, +}; +use ethers_providers::{JsonRpcClient, Provider}; +use ethers_signers::{Client, Signer}; +use ethers_types::{ + Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256, +}; + +use rustc_hex::ToHex; +use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; + +use thiserror::Error as ThisError; + +/// Represents a contract instance at an address. Provides methods for +/// contract interaction. +#[derive(Debug, Clone)] +pub struct Contract<'a, S, P> { + client: &'a Client<'a, S, P>, + abi: &'a Abi, + address: Address, + + /// A mapping from method signature to a name-index pair for accessing + /// functions in the contract ABI. This is used to avoid allocation when + /// searching for matching functions by signature. + // Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs + methods: HashMap, +} + +impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> { + /// Creates a new contract from the provided client, abi and address + pub fn new(client: &'a Client<'a, S, P>, abi: &'a Abi, address: Address) -> Self { + let methods = create_mapping(&abi.functions, |function| function.selector()); + + Self { + client, + abi, + address, + methods, + } + } + + /// Returns a transaction builder for the provided function name. If there are + /// multiple functions with the same name due to overloading, consider using + /// the `method_hash` method instead, since this will use the first match. + pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result, Error> + where + 'a: 'b, + { + // get the event's full name + let event = self.abi.event(name)?; + Ok(Event { + provider: &self.client.provider(), + filter: Filter::new().event(&event.abi_signature()), + event: &event, + datatype: PhantomData, + }) + } + + /// Returns a transaction builder for the provided function name. If there are + /// multiple functions with the same name due to overloading, consider using + /// the `method_hash` method instead, since this will use the first match. + pub fn method( + &self, + name: &str, + args: T, + ) -> Result, Error> { + // get the function + let function = self.abi.function(name)?; + self.method_func(function, args) + } + + /// Returns a transaction builder for the selected function signature. This should be + /// preferred if there are overloaded functions in your smart contract + pub fn method_hash( + &self, + signature: Selector, + args: T, + ) -> Result, Error> { + let function = self + .methods + .get(&signature) + .map(|(name, index)| &self.abi.functions[name][*index]) + .ok_or_else(|| Error::InvalidName(signature.to_hex::()))?; + self.method_func(function, args) + } + + fn method_func( + &self, + function: &Function, + args: T, + ) -> Result, Error> { + // create the calldata + let data = function.encode_input(&args.into_tokens())?; + + // create the tx object + let tx = TransactionRequest { + to: Some(self.address), + data: Some(data.into()), + ..Default::default() + }; + + Ok(Sender { + tx, + client: self.client, + block: None, + function: function.to_owned(), + datatype: PhantomData, + }) + } + + pub fn address(&self) -> &Address { + &self.address + } + + pub fn abi(&self) -> &Abi { + &self.abi + } +} + +pub struct Sender<'a, S, P, D> { + tx: TransactionRequest, + function: Function, + client: &'a Client<'a, S, P>, + block: Option, + datatype: PhantomData, +} + +impl<'a, S, P, D: Detokenize> Sender<'a, S, P, D> { + /// Sets the `from` field in the transaction to the provided value + pub fn from>(mut self, from: T) -> Self { + self.tx.from = Some(from.into()); + self + } + + /// Sets the `gas` field in the transaction to the provided value + pub fn gas>(mut self, gas: T) -> Self { + self.tx.gas = Some(gas.into()); + self + } + + /// Sets the `gas_price` field in the transaction to the provided value + pub fn gas_price>(mut self, gas_price: T) -> Self { + self.tx.gas_price = Some(gas_price.into()); + self + } + + /// Sets the `value` field in the transaction to the provided value + pub fn value>(mut self, value: T) -> Self { + self.tx.value = Some(value.into()); + self + } +} + +#[derive(ThisError, Debug)] +// TODO: Can we get rid of this static? +pub enum ContractError +where + P::Error: 'static, +{ + #[error(transparent)] + DecodingError(#[from] ethers_abi::Error), + #[error(transparent)] + DetokenizationError(#[from] ethers_abi::InvalidOutputType), + #[error(transparent)] + CallError(P::Error), +} + +impl<'a, S: Signer, P: JsonRpcClient, D: Detokenize> Sender<'a, S, P, D> +where + P::Error: 'static, +{ + pub async fn call(self) -> Result> { + let bytes = self + .client + .call(self.tx, self.block) + .await + .map_err(ContractError::CallError)?; + + let tokens = self.function.decode_output(&bytes.0)?; + + let data = D::from_tokens(tokens)?; + + Ok(data) + } + + pub async fn send(self) -> Result { + self.client.send_transaction(self.tx, self.block).await + } +} + +pub struct Event<'a, 'b, P, D> { + pub filter: Filter, + provider: &'a Provider

, + event: &'b AbiEvent, + datatype: PhantomData, +} + +impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> { + pub fn from_block>(mut self, block: T) -> Self { + self.filter.from_block = Some(block.into()); + self + } + + pub fn to_block>(mut self, block: T) -> Self { + self.filter.to_block = Some(block.into()); + self + } + + pub fn topic0>>(mut self, topic: T) -> Self { + self.filter.topics.push(topic.into()); + self + } + + pub fn topics(mut self, topics: &[ValueOrArray]) -> Self { + self.filter.topics.extend_from_slice(topics); + self + } +} + +// TODO: Can we get rid of the static? +impl<'a, 'b, P: JsonRpcClient, D: Detokenize> Event<'a, 'b, P, D> +where + P::Error: 'static, +{ + pub async fn query(self) -> Result, ContractError

> { + // get the logs + let logs = self + .provider + .get_logs(&self.filter) + .await + .map_err(ContractError::CallError)?; + + let events = logs + .into_iter() + .map(|log| { + // ethabi parses the unindexed and indexed logs together to a + // vector of tokens + let tokens = self + .event + .parse_log(RawLog { + topics: log.topics, + data: log.data.0, + })? + .params + .into_iter() + .map(|param| param.value) + .collect::>(); + + // convert the tokens to the requested datatype + Ok::<_, ContractError

>(D::from_tokens(tokens)?) + }) + .collect::, _>>()?; + + Ok(events) + } +} + +/// Utility function for creating a mapping between a unique signature and a +/// name-index pair for accessing contract ABI items. +fn create_mapping( + elements: &HashMap>, + signature: F, +) -> HashMap +where + S: Hash + Eq, + F: Fn(&T) -> S, +{ + let signature = &signature; + elements + .iter() + .flat_map(|(name, sub_elements)| { + sub_elements + .iter() + .enumerate() + .map(move |(index, element)| (signature(element), (name.to_owned(), index))) + }) + .collect() +} diff --git a/crates/ethers-contract/src/lib.rs b/crates/ethers-contract/src/lib.rs index f020b60b..157109ed 100644 --- a/crates/ethers-contract/src/lib.rs +++ b/crates/ethers-contract/src/lib.rs @@ -1,284 +1,15 @@ -use ethers_abi::{ - Abi, Detokenize, Error, Event as AbiEvent, EventExt, Function, FunctionExt, RawLog, Tokenize, -}; -use ethers_providers::{JsonRpcClient, Provider}; -use ethers_signers::{Client, Signer}; -use ethers_types::{ - Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256, -}; +mod contract; +pub use contract::*; -use rustc_hex::ToHex; -use std::{collections::HashMap, fmt::Debug, hash::Hash}; -use thiserror::Error as ThisError; +#[cfg(feature = "abigen")] +pub use ethers_contract_abigen::Builder; -/// Represents a contract instance at an address. Provides methods for -/// contract interaction. -#[derive(Debug, Clone)] -pub struct Contract<'a, S, P> { - client: &'a Client<'a, S, P>, - abi: Abi, - address: Address, +#[cfg(feature = "abigen")] +pub use ethers_contract_derive::abigen; - /// A mapping from method signature to a name-index pair for accessing - /// functions in the contract ABI. This is used to avoid allocation when - /// searching for matching functions by signature. - // Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs - methods: HashMap, -} - -use std::marker::PhantomData; - -impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> { - /// Creates a new contract from the provided client, abi and address - pub fn new(client: &'a Client<'a, S, P>, abi: Abi, address: Address) -> Self { - let methods = create_mapping(&abi.functions, |function| function.selector()); - - Self { - client, - abi, - address, - methods, - } - } - - /// Returns a transaction builder for the provided function name. If there are - /// multiple functions with the same name due to overloading, consider using - /// the `method_hash` method instead, since this will use the first match. - pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result, Error> - where - 'a: 'b, - { - // get the event's full name - let event = self.abi.event(name)?; - Ok(Event { - provider: &self.client.provider(), - filter: Filter::new().event(&event.abi_signature()), - event: &event, - datatype: PhantomData, - }) - } - - /// Returns a transaction builder for the provided function name. If there are - /// multiple functions with the same name due to overloading, consider using - /// the `method_hash` method instead, since this will use the first match. - pub fn method( - &self, - name: &str, - args: Option, - ) -> Result, Error> { - // get the function - let function = self.abi.function(name)?; - self.method_func(function, args) - } - - /// Returns a transaction builder for the selected function signature. This should be - /// preferred if there are overloaded functions in your smart contract - pub fn method_hash( - &self, - signature: Selector, - args: Option, - ) -> Result, Error> { - let function = self - .methods - .get(&signature) - .map(|(name, index)| &self.abi.functions[name][*index]) - .ok_or_else(|| Error::InvalidName(signature.to_hex::()))?; - self.method_func(function, args) - } - - fn method_func( - &self, - function: &Function, - args: Option, - ) -> Result, Error> { - // create the calldata - let data = if let Some(args) = args { - function.encode_input(&args.into_tokens())? - } else { - function.selector().to_vec() - }; - - // create the tx object - let tx = TransactionRequest { - to: Some(self.address), - data: Some(data.into()), - ..Default::default() - }; - - Ok(Sender { - tx, - client: self.client, - block: None, - function: function.to_owned(), - datatype: PhantomData, - }) - } - - pub fn address(&self) -> &Address { - &self.address - } - - pub fn abi(&self) -> &Abi { - &self.abi - } -} - -pub struct Sender<'a, S, P, D> { - tx: TransactionRequest, - function: Function, - client: &'a Client<'a, S, P>, - block: Option, - datatype: PhantomData, -} - -impl<'a, S, P, D: Detokenize> Sender<'a, S, P, D> { - /// Sets the `from` field in the transaction to the provided value - pub fn from>(mut self, from: T) -> Self { - self.tx.from = Some(from.into()); - self - } - - /// Sets the `gas` field in the transaction to the provided value - pub fn gas>(mut self, gas: T) -> Self { - self.tx.gas = Some(gas.into()); - self - } - - /// Sets the `gas_price` field in the transaction to the provided value - pub fn gas_price>(mut self, gas_price: T) -> Self { - self.tx.gas_price = Some(gas_price.into()); - self - } - - /// Sets the `value` field in the transaction to the provided value - pub fn value>(mut self, value: T) -> Self { - self.tx.value = Some(value.into()); - self - } -} - -#[derive(ThisError, Debug)] -// TODO: Can we get rid of this static? -pub enum ContractError -where - P::Error: 'static, -{ - #[error(transparent)] - DecodingError(#[from] ethers_abi::Error), - #[error(transparent)] - DetokenizationError(#[from] ethers_abi::InvalidOutputType), - #[error(transparent)] - CallError(P::Error), -} - -impl<'a, S: Signer, P: JsonRpcClient, D: Detokenize> Sender<'a, S, P, D> -where - P::Error: 'static, -{ - pub async fn call(self) -> Result> { - let bytes = self - .client - .call(self.tx, self.block) - .await - .map_err(ContractError::CallError)?; - - let tokens = self.function.decode_output(&bytes.0)?; - - let data = D::from_tokens(tokens)?; - - Ok(data) - } - - pub async fn send(self) -> Result { - self.client.send_transaction(self.tx, self.block).await - } -} - -pub struct Event<'a, 'b, P, D> { - filter: Filter, - provider: &'a Provider

, - event: &'b AbiEvent, - datatype: PhantomData, -} - -impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> { - pub fn from_block>(mut self, block: T) -> Self { - self.filter.from_block = Some(block.into()); - self - } - - pub fn to_block>(mut self, block: T) -> Self { - self.filter.to_block = Some(block.into()); - self - } - - pub fn topic>>(mut self, topic: T) -> Self { - self.filter.topics.push(topic.into()); - self - } - - pub fn topics(mut self, topics: &[ValueOrArray]) -> Self { - self.filter.topics.extend_from_slice(topics); - self - } -} - -// TODO: Can we get rid of the static? -impl<'a, 'b, P: JsonRpcClient, D: Detokenize> Event<'a, 'b, P, D> -where - P::Error: 'static, -{ - pub async fn query(self) -> Result, ContractError

> { - // get the logs - let logs = self - .provider - .get_logs(&self.filter) - .await - .map_err(ContractError::CallError)?; - - let events = logs - .into_iter() - .map(|log| { - // ethabi parses the unindexed and indexed logs together to a - // vector of tokens - let tokens = self - .event - .parse_log(RawLog { - topics: log.topics, - data: log.data.0, - })? - .params - .into_iter() - .map(|param| param.value) - .collect::>(); - - // convert the tokens to the requested datatype - Ok::<_, ContractError

>(D::from_tokens(tokens)?) - }) - .collect::, _>>()?; - - Ok(events) - } -} - -/// Utility function for creating a mapping between a unique signature and a -/// name-index pair for accessing contract ABI items. -fn create_mapping( - elements: &HashMap>, - signature: F, -) -> HashMap -where - S: Hash + Eq, - F: Fn(&T) -> S, -{ - let signature = &signature; - elements - .iter() - .flat_map(|(name, sub_elements)| { - sub_elements - .iter() - .enumerate() - .map(move |(index, element)| (signature(element), (name.to_owned(), index))) - }) - .collect() -} +// re-export for convenience +pub use ethers_abi as abi; +pub use ethers_providers as providers; +pub use ethers_signers as signers; +pub use ethers_types as types; +pub use once_cell::sync::Lazy; diff --git a/crates/ethers-derive/src/src/contract.rs b/crates/ethers-derive/src/src/contract.rs deleted file mode 100644 index 2c955bd5..00000000 --- a/crates/ethers-derive/src/src/contract.rs +++ /dev/null @@ -1,178 +0,0 @@ -#![deny(missing_docs)] - -//! Crate for generating type-safe bindings to Ethereum smart contracts. This -//! crate is intended to be used either indirectly with the `ethcontract` -//! crate's `contract` procedural macro or directly from a build script. - -mod common; -mod deployment; -mod events; -mod methods; -mod types; - -use crate::util; -use crate::Args; -use anyhow::{anyhow, Context as _, Result}; -use ethcontract_common::{Address, Artifact}; -use inflector::Inflector; -use proc_macro2::{Ident, Literal, TokenStream}; -use quote::quote; -use std::collections::HashMap; -use syn::{Path, Visibility}; - -/// Internal shared context for generating smart contract bindings. -pub(crate) struct Context { - /// The artifact JSON as string literal. - artifact_json: Literal, - /// The parsed artifact. - artifact: Artifact, - /// The identifier for the runtime crate. Usually this is `ethcontract` but - /// it can be different if the crate was renamed in the Cargo manifest for - /// example. - runtime_crate: Ident, - /// The visibility for the generated module and re-exported contract type. - visibility: Visibility, - /// The name of the module as an identifier in which to place the contract - /// implementation. Note that the main contract type gets re-exported in the - /// root. - contract_mod: Ident, - /// The contract name as an identifier. - contract_name: Ident, - /// Additional contract deployments. - deployments: HashMap, - /// Manually specified method aliases. - method_aliases: HashMap, - /// Derives added to event structs and enums. - event_derives: Vec, -} - -impl Context { - /// Create a context from the code generation arguments. - fn from_args(args: Args) -> Result { - let (artifact_json, artifact) = { - let artifact_json = args - .artifact_source - .artifact_json() - .context("failed to get artifact JSON")?; - - let artifact = Artifact::from_json(&artifact_json) - .with_context(|| format!("invalid artifact JSON '{}'", artifact_json)) - .with_context(|| { - format!( - "failed to parse artifact from source {:?}", - args.artifact_source, - ) - })?; - - (Literal::string(&artifact_json), artifact) - }; - - let raw_contract_name = if let Some(name) = args.contract_name_override.as_ref() { - name - } else if !artifact.contract_name.is_empty() { - &artifact.contract_name - } else { - return Err(anyhow!( - "contract artifact is missing a name, this can happen when \ - using a source that does not provide a contract name such as \ - Etherscan; in this case the contract must be manually \ - specified" - )); - }; - - let runtime_crate = util::ident(&args.runtime_crate_name); - let visibility = match args.visibility_modifier.as_ref() { - Some(vis) => syn::parse_str(vis)?, - None => Visibility::Inherited, - }; - let contract_mod = if let Some(name) = args.contract_mod_override.as_ref() { - util::ident(name) - } else { - util::ident(&raw_contract_name.to_snake_case()) - }; - let contract_name = util::ident(raw_contract_name); - - // NOTE: We only check for duplicate signatures here, since if there are - // duplicate aliases, the compiler will produce a warning because a - // method will be re-defined. - let mut method_aliases = HashMap::new(); - for (signature, alias) in args.method_aliases.into_iter() { - let alias = syn::parse_str(&alias)?; - if method_aliases.insert(signature.clone(), alias).is_some() { - return Err(anyhow!( - "duplicate method signature '{}' in method aliases", - signature, - )); - } - } - - let event_derives = args - .event_derives - .iter() - .map(|derive| syn::parse_str::(derive)) - .collect::, _>>() - .context("failed to parse event derives")?; - - Ok(Context { - artifact_json, - artifact, - runtime_crate, - visibility, - contract_mod, - contract_name, - deployments: args.deployments, - method_aliases, - event_derives, - }) - } -} - -#[cfg(test)] -impl Default for Context { - fn default() -> Self { - Context { - artifact_json: Literal::string("{}"), - artifact: Artifact::empty(), - runtime_crate: util::ident("ethcontract"), - visibility: Visibility::Inherited, - contract_mod: util::ident("contract"), - contract_name: util::ident("Contract"), - deployments: HashMap::new(), - method_aliases: HashMap::new(), - event_derives: Vec::new(), - } - } -} - -pub(crate) fn expand(args: Args) -> Result { - let cx = Context::from_args(args)?; - let contract = expand_contract(&cx).context("error expanding contract from its ABI")?; - - Ok(contract) -} - -fn expand_contract(cx: &Context) -> Result { - let runtime_crate = &cx.runtime_crate; - let vis = &cx.visibility; - let contract_mod = &cx.contract_mod; - let contract_name = &cx.contract_name; - - let common = common::expand(cx); - let deployment = deployment::expand(cx)?; - let methods = methods::expand(cx)?; - let events = events::expand(cx)?; - - Ok(quote! { - #[allow(dead_code)] - #vis mod #contract_mod { - #[rustfmt::skip] - use #runtime_crate as ethcontract; - - #common - #deployment - #methods - #events - } - #vis use self::#contract_mod::Contract as #contract_name; - }) -} diff --git a/crates/ethers-derive/src/src/contract/common.rs b/crates/ethers-derive/src/src/contract/common.rs deleted file mode 100644 index d2c86b61..00000000 --- a/crates/ethers-derive/src/src/contract/common.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::contract::Context; -use crate::util::expand_doc; -use ethcontract_common::Address; -use proc_macro2::{Literal, TokenStream}; -use quote::quote; - -pub(crate) fn expand(cx: &Context) -> TokenStream { - let artifact_json = &cx.artifact_json; - let contract_name = &cx.contract_name; - - let doc_str = cx - .artifact - .devdoc - .details - .as_deref() - .unwrap_or("Generated by `ethcontract`"); - let doc = expand_doc(doc_str); - - let deployments = cx.deployments.iter().map(|(network_id, address)| { - let network_id = Literal::string(&network_id.to_string()); - let address = expand_address(*address); - - quote! { - artifact.networks.insert( - #network_id.to_owned(), - self::ethcontract::common::truffle::Network { - address: #address, - transaction_hash: None, - }, - ); - } - }); - - quote! { - #doc - #[derive(Clone)] - pub struct Contract { - methods: Methods, - } - - impl Contract { - /// Retrieves the truffle artifact used to generate the type safe - /// API for this contract. - pub fn artifact() -> &'static self::ethcontract::Artifact { - use self::ethcontract::private::lazy_static; - use self::ethcontract::Artifact; - - lazy_static! { - pub static ref ARTIFACT: Artifact = { - #[allow(unused_mut)] - let mut artifact = Artifact::from_json(#artifact_json) - .expect("valid artifact JSON"); - #( #deployments )* - - artifact - }; - } - &ARTIFACT - } - - /// Creates a new contract instance with the specified `web3` - /// provider at the given `Address`. - /// - /// Note that this does not verify that a contract with a maching - /// `Abi` is actually deployed at the given address. - pub fn at( - web3: &self::ethcontract::web3::api::Web3, - address: self::ethcontract::Address, - ) -> Self - where - F: self::ethcontract::web3::futures::Future< - Item = self::ethcontract::json::Value, - Error = self::ethcontract::web3::Error, - > + Send + 'static, - T: self::ethcontract::web3::Transport + Send + Sync + 'static, - { - Contract::with_transaction(web3, address, None) - } - - - /// Creates a new contract instance with the specified `web3` provider with - /// the given `Abi` at the given `Address` and an optional transaction hash. - /// This hash is used to retrieve contract related information such as the - /// creation block (which is useful for fetching all historic events). - /// - /// Note that this does not verify that a contract with a matching `Abi` is - /// actually deployed at the given address nor that the transaction hash, - /// when provided, is actually for this contract deployment. - pub fn with_transaction( - web3: &self::ethcontract::web3::api::Web3, - address: self::ethcontract::Address, - transaction_hash: Option, - ) -> Self - where - F: self::ethcontract::web3::futures::Future< - Item = self::ethcontract::json::Value, - Error = self::ethcontract::web3::Error, - > + Send + 'static, - T: self::ethcontract::web3::Transport + Send + Sync + 'static, - { - use self::ethcontract::Instance; - use self::ethcontract::transport::DynTransport; - use self::ethcontract::web3::api::Web3; - - let transport = DynTransport::new(web3.transport().clone()); - let web3 = Web3::new(transport); - let abi = Self::artifact().abi.clone(); - let instance = Instance::with_transaction(web3, abi, address, transaction_hash); - - Contract::from_raw(instance) - } - - /// Creates a contract from a raw instance. - fn from_raw(instance: self::ethcontract::dyns::DynInstance) -> Self { - let methods = Methods { instance }; - Contract { methods } - } - - /// Returns the contract address being used by this instance. - pub fn address(&self) -> self::ethcontract::Address { - self.raw_instance().address() - } - - /// Returns the hash for the transaction that deployed the contract - /// if it is known, `None` otherwise. - pub fn transaction_hash(&self) -> Option { - self.raw_instance().transaction_hash() - } - - /// Returns a reference to the default method options used by this - /// contract. - pub fn defaults(&self) -> &self::ethcontract::contract::MethodDefaults { - &self.raw_instance().defaults - } - - /// Returns a mutable reference to the default method options used - /// by this contract. - pub fn defaults_mut(&mut self) -> &mut self::ethcontract::contract::MethodDefaults { - &mut self.raw_instance_mut().defaults - } - - /// Returns a reference to the raw runtime instance used by this - /// contract. - pub fn raw_instance(&self) -> &self::ethcontract::dyns::DynInstance { - &self.methods.instance - } - - /// Returns a mutable reference to the raw runtime instance used by - /// this contract. - fn raw_instance_mut(&mut self) -> &mut self::ethcontract::dyns::DynInstance { - &mut self.methods.instance - } - } - - impl std::fmt::Debug for Contract { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.debug_tuple(stringify!(#contract_name)) - .field(&self.address()) - .finish() - } - } - } -} - -/// Expands an `Address` into a literal representation that can be used with -/// quasi-quoting for code generation. -fn expand_address(address: Address) -> TokenStream { - let bytes = address - .as_bytes() - .iter() - .copied() - .map(Literal::u8_unsuffixed); - - quote! { - self::ethcontract::H160([#( #bytes ),*]) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[rustfmt::skip] - fn expand_address_value() { - assert_quote!( - expand_address(Address::zero()), - { - self::ethcontract::H160([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]) - }, - ); - - assert_quote!( - expand_address("000102030405060708090a0b0c0d0e0f10111213".parse().unwrap()), - { - self::ethcontract::H160([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) - }, - ); - } -} diff --git a/crates/ethers-derive/src/src/contract/deployment.rs b/crates/ethers-derive/src/src/contract/deployment.rs deleted file mode 100644 index d05faeb2..00000000 --- a/crates/ethers-derive/src/src/contract/deployment.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::contract::{methods, Context}; -use crate::util; -use anyhow::{Context as _, Result}; -use inflector::Inflector; -use proc_macro2::{Literal, TokenStream}; -use quote::quote; - -pub(crate) fn expand(cx: &Context) -> Result { - let deployed = expand_deployed(&cx); - let deploy = - expand_deploy(&cx).context("error generating contract `deploy` associated function")?; - - Ok(quote! { - #deployed - #deploy - }) -} - -fn expand_deployed(cx: &Context) -> TokenStream { - if cx.artifact.networks.is_empty() && cx.deployments.is_empty() { - return quote! {}; - } - - quote! { - impl Contract { - /// Locates a deployed contract based on the current network ID - /// reported by the `web3` provider. - /// - /// Note that this does not verify that a contract with a maching - /// `Abi` is actually deployed at the given address. - pub async fn deployed( - web3: &self::ethcontract::web3::api::Web3, - ) -> Result - where - F: self::ethcontract::web3::futures::Future< - Item = self::ethcontract::json::Value, - Error = self::ethcontract::web3::Error - > + Send + 'static, - T: self::ethcontract::web3::Transport + Send + Sync + 'static, - { - use self::ethcontract::{Instance, Web3}; - use self::ethcontract::transport::DynTransport; - - let transport = DynTransport::new(web3.transport().clone()); - let web3 = Web3::new(transport); - let instance = Instance::deployed(web3, Contract::artifact().clone()).await?; - - Ok(Contract::from_raw(instance)) - } - } - } -} - -fn expand_deploy(cx: &Context) -> Result { - if cx.artifact.bytecode.is_empty() { - // do not generate deploy method for contracts that have empty bytecode - return Ok(quote! {}); - } - - // TODO(nlordell): not sure how contructor documentation get generated as I - // can't seem to get truffle to output it - let doc = util::expand_doc("Generated by `ethcontract`"); - - let (input, arg) = match cx.artifact.abi.constructor() { - Some(contructor) => ( - methods::expand_inputs(&contructor.inputs)?, - methods::expand_inputs_call_arg(&contructor.inputs), - ), - None => (quote! {}, quote! {()}), - }; - - let libs: Vec<_> = cx - .artifact - .bytecode - .undefined_libraries() - .map(|name| (name, util::safe_ident(&name.to_snake_case()))) - .collect(); - let (lib_struct, lib_input, link) = if !libs.is_empty() { - let lib_struct = { - let lib_struct_fields = libs.iter().map(|(name, field)| { - let doc = util::expand_doc(&format!("Address of the `{}` library.", name)); - - quote! { - #doc pub #field: self::ethcontract::Address - } - }); - - quote! { - /// Undefinied libraries in the contract bytecode that are - /// required for linking in order to deploy. - pub struct Libraries { - #( #lib_struct_fields, )* - } - } - }; - - let link = { - let link_libraries = libs.iter().map(|(name, field)| { - let name_lit = Literal::string(&name); - - quote! { - bytecode.link(#name_lit, libs.#field).expect("valid library"); - } - }); - - quote! { - let mut bytecode = bytecode; - #( #link_libraries )* - } - }; - - (lib_struct, quote! { , libs: Libraries }, link) - } else { - Default::default() - }; - - Ok(quote! { - #lib_struct - - impl Contract { - #doc - pub fn builder( - web3: &self::ethcontract::web3::api::Web3 #lib_input #input , - ) -> self::ethcontract::dyns::DynDeployBuilder - where - F: self::ethcontract::web3::futures::Future< - Item = self::ethcontract::json::Value, - Error = self::ethcontract::web3::Error, - > + Send + 'static, - T: self::ethcontract::web3::Transport + Send + Sync + 'static, - { - use self::ethcontract::dyns::DynTransport; - use self::ethcontract::contract::DeployBuilder; - use self::ethcontract::web3::api::Web3; - - let transport = DynTransport::new(web3.transport().clone()); - let web3 = Web3::new(transport); - - let bytecode = Self::artifact().bytecode.clone(); - #link - - DeployBuilder::new(web3, bytecode, #arg).expect("valid deployment args") - } - } - - impl self::ethcontract::contract::Deploy for Contract { - type Context = self::ethcontract::common::Bytecode; - - fn bytecode(cx: &Self::Context) -> &self::ethcontract::common::Bytecode { - cx - } - - fn abi(_: &Self::Context) -> &self::ethcontract::common::Abi { - &Self::artifact().abi - } - - fn from_deployment( - web3: self::ethcontract::dyns::DynWeb3, - address: self::ethcontract::Address, - transaction_hash: self::ethcontract::H256, - _: Self::Context, - ) -> Self { - Self::with_transaction(&web3, address, Some(transaction_hash)) - } - } - }) -} diff --git a/crates/ethers-derive/src/src/contract/events.rs b/crates/ethers-derive/src/src/contract/events.rs deleted file mode 100644 index 07a26229..00000000 --- a/crates/ethers-derive/src/src/contract/events.rs +++ /dev/null @@ -1,855 +0,0 @@ -use crate::contract::{types, Context}; -use crate::util; -use anyhow::Result; -use ethcontract_common::abi::{Event, EventParam, Hash, ParamType}; -use ethcontract_common::abiext::EventExt; -use inflector::Inflector; -use proc_macro2::{Literal, TokenStream}; -use quote::quote; -use syn::Path; - -pub(crate) fn expand(cx: &Context) -> Result { - let structs_mod = expand_structs_mod(cx)?; - let filters = expand_filters(cx)?; - let all_events = expand_all_events(cx); - - Ok(quote! { - #structs_mod - #filters - #all_events - }) -} - -/// Expands into a module containing all the event data structures from the ABI. -fn expand_structs_mod(cx: &Context) -> Result { - let data_types = cx - .artifact - .abi - .events() - .map(|event| expand_data_type(event, &cx.event_derives)) - .collect::>>()?; - if data_types.is_empty() { - return Ok(quote! {}); - } - - Ok(quote! { - /// Module containing all generated data models for this contract's - /// events. - pub mod event_data { - use super::ethcontract; - - #( #data_types )* - } - }) -} - -fn expand_derives(derives: &[Path]) -> TokenStream { - quote! {#(#derives),*} -} - -/// Expands an ABI event into a single event data type. This can expand either -/// into a structure or a tuple in the case where all event parameters (topics -/// and data) are anonymous. -fn expand_data_type(event: &Event, event_derives: &[Path]) -> Result { - let event_name = expand_struct_name(event); - - let signature = expand_hash(event.signature()); - - let abi_signature = event.abi_signature(); - let abi_signature_lit = Literal::string(&abi_signature); - let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature)); - - let params = expand_params(event)?; - - let all_anonymous_fields = event.inputs.iter().all(|input| input.name.is_empty()); - let (data_type_definition, data_type_construction) = if all_anonymous_fields { - expand_data_tuple(&event_name, ¶ms) - } else { - expand_data_struct(&event_name, ¶ms) - }; - - let params_len = Literal::usize_unsuffixed(params.len()); - let read_param_token = params - .iter() - .map(|(name, ty)| { - quote! { - let #name = <#ty as self::ethcontract::web3::contract::tokens::Tokenizable> - ::from_token(tokens.next().unwrap())?; - } - }) - .collect::>(); - - let derives = expand_derives(event_derives); - - Ok(quote! { - #[derive(Clone, Debug, Default, Eq, PartialEq, #derives)] - pub #data_type_definition - - impl #event_name { - /// Retrieves the signature for the event this data corresponds to. - /// This signature is the Keccak-256 hash of the ABI signature of - /// this event. - pub fn signature() -> self::ethcontract::H256 { - #signature - } - - /// Retrieves the ABI signature for the event this data corresponds - /// to. For this event the value should always be: - /// - #abi_signature_doc - pub fn abi_signature() -> &'static str { - #abi_signature_lit - } - } - - impl self::ethcontract::web3::contract::tokens::Detokenize for #event_name { - fn from_tokens( - tokens: Vec, - ) -> Result { - if tokens.len() != #params_len { - return Err(self::ethcontract::web3::contract::Error::InvalidOutputType(format!( - "Expected {} tokens, got {}: {:?}", - #params_len, - tokens.len(), - tokens - ))); - } - - #[allow(unused_mut)] - let mut tokens = tokens.into_iter(); - #( #read_param_token )* - - Ok(#data_type_construction) - } - } - }) -} - -/// Expands an ABI event into an identifier for its event data type. -fn expand_struct_name(event: &Event) -> TokenStream { - let event_name = util::ident(&event.name.to_pascal_case()); - quote! { #event_name } -} - -/// Expands an ABI event into name-type pairs for each of its parameters. -fn expand_params(event: &Event) -> Result> { - event - .inputs - .iter() - .enumerate() - .map(|(i, input)| { - // NOTE: Events can contain nameless values. - let name = util::expand_input_name(i, &input.name); - let ty = expand_input_type(&input)?; - - Ok((name, ty)) - }) - .collect() -} - -/// Expands an event data structure from its name-type parameter pairs. Returns -/// a tuple with the type definition (i.e. the struct declaration) and -/// construction (i.e. code for creating an instance of the event data). -fn expand_data_struct( - name: &TokenStream, - params: &[(TokenStream, TokenStream)], -) -> (TokenStream, TokenStream) { - let fields = params - .iter() - .map(|(name, ty)| quote! { pub #name: #ty }) - .collect::>(); - - let param_names = params - .iter() - .map(|(name, _)| name) - .cloned() - .collect::>(); - - let definition = quote! { struct #name { #( #fields, )* } }; - let construction = quote! { #name { #( #param_names ),* } }; - - (definition, construction) -} - -/// Expands an event data named tuple from its name-type parameter pairs. -/// Returns a tuple with the type definition and construction. -fn expand_data_tuple( - name: &TokenStream, - params: &[(TokenStream, TokenStream)], -) -> (TokenStream, TokenStream) { - let fields = params - .iter() - .map(|(_, ty)| quote! { pub #ty }) - .collect::>(); - - let param_names = params - .iter() - .map(|(name, _)| name) - .cloned() - .collect::>(); - - let definition = quote! { struct #name( #( #fields ),* ); }; - let construction = quote! { #name( #( #param_names ),* ) }; - - (definition, construction) -} - -/// Expands into an `Events` type with method definitions for creating event -/// streams for all non-anonymous contract events in the ABI. -fn expand_filters(cx: &Context) -> Result { - let standard_events = cx - .artifact - .abi - .events() - .filter(|event| !event.anonymous) - .collect::>(); - if standard_events.is_empty() { - return Ok(quote! {}); - } - - let filters = standard_events - .iter() - .map(|event| expand_filter(event)) - .collect::>>()?; - let builders = standard_events - .iter() - .map(|event| expand_builder_type(event)) - .collect::>>()?; - - Ok(quote! { - impl Contract { - /// Retrieves a handle to a type containing for creating event - /// streams for all the contract events. - pub fn events(&self) -> Events<'_> { - Events { - instance: self.raw_instance(), - } - } - } - - pub struct Events<'a> { - instance: &'a self::ethcontract::dyns::DynInstance, - } - - impl Events<'_> { - #( #filters )* - } - - /// Module containing the generated event stream builders with type safe - /// filter methods for this contract's events. - pub mod event_builders { - use super::ethcontract; - use super::event_data; - - #( #builders )* - } - }) -} - -/// Expands into a single method for contracting an event stream. -fn expand_filter(event: &Event) -> Result { - let name = util::safe_ident(&event.name.to_snake_case()); - let builder_name = expand_builder_name(event); - let signature = expand_hash(event.signature()); - - Ok(quote! { - /// Generated by `ethcontract`. - pub fn #name(&self) -> self::event_builders::#builder_name { - self::event_builders::#builder_name( - self.instance.event(#signature) - .expect("generated event filter"), - ) - } - }) -} - -/// Expands an ABI event into a wrapped `EventBuilder` type with type-safe -/// filter methods. -fn expand_builder_type(event: &Event) -> Result { - let event_name = expand_struct_name(event); - let builder_doc = util::expand_doc(&format!( - "A builder for creating a filtered stream of `{}` events.", - event_name - )); - let builder_name = expand_builder_name(event); - let topic_filters = expand_builder_topic_filters(event)?; - - Ok(quote! { - #builder_doc - pub struct #builder_name( - /// The inner event builder. - pub self::ethcontract::dyns::DynEventBuilder, - ); - - impl #builder_name { - /// Sets the starting block from which to stream logs for. - /// - /// If left unset defaults to the latest block. - #[allow(clippy::wrong_self_convention)] - pub fn from_block(mut self, block: self::ethcontract::BlockNumber) -> Self { - self.0 = (self.0).from_block(block); - self - } - - /// Sets the last block from which to stream logs for. - /// - /// If left unset defaults to the streaming until the end of days. - #[allow(clippy::wrong_self_convention)] - pub fn to_block(mut self, block: self::ethcontract::BlockNumber) -> Self { - self.0 = (self.0).to_block(block); - self - } - - /// Limit the number of events that can be retrieved by this filter. - /// - /// Note that this parameter is non-standard. - pub fn limit(mut self, value: usize) -> Self { - self.0 = (self.0).limit(value); - self - } - - /// The polling interval. This is used as the interval between - /// consecutive `eth_getFilterChanges` calls to get filter updates. - pub fn poll_interval(mut self, value: std::time::Duration) -> Self { - self.0 = (self.0).poll_interval(value); - self - } - - #topic_filters - - /// Returns a future that resolves with a collection of all existing - /// logs matching the builder parameters. - pub async fn query(self) -> std::result::Result< - std::vec::Vec>, - self::ethcontract::errors::EventError, - > { - (self.0).query().await - } - - /// Creates an event stream from the current event builder. - pub fn stream(self) -> impl self::ethcontract::futures::stream::Stream< - Item = std::result::Result< - self::ethcontract::StreamEvent, - self::ethcontract::errors::EventError, - >, - > { - (self.0).stream() - } - } - }) -} - -/// Expands an ABI event into filter methods for its indexed parameters. -fn expand_builder_topic_filters(event: &Event) -> Result { - let topic_filters = event - .inputs - .iter() - .filter(|input| input.indexed) - .enumerate() - .map(|(topic_index, input)| expand_builder_topic_filter(topic_index, input)) - .collect::>>()?; - - Ok(quote! { - #( #topic_filters )* - }) -} - -/// Expands a event parameter into an event builder filter method for the -/// specified topic index. -fn expand_builder_topic_filter(topic_index: usize, param: &EventParam) -> Result { - let doc = util::expand_doc(&format!( - "Adds a filter for the {} event parameter.", - param.name, - )); - let topic = util::ident(&format!("topic{}", topic_index)); - let name = if param.name.is_empty() { - topic.clone() - } else { - util::safe_ident(¶m.name.to_snake_case()) - }; - let ty = expand_input_type(¶m)?; - - Ok(quote! { - #doc - pub fn #name(mut self, topic: self::ethcontract::Topic<#ty>) -> Self { - self.0 = (self.0).#topic(topic); - self - } - }) -} - -/// Expands an ABI event into an identifier for its event data type. -fn expand_builder_name(event: &Event) -> TokenStream { - let builder_name = util::ident(&format!("{}Builder", &event.name.to_pascal_case())); - quote! { #builder_name } -} - -/// Expands into the `all_events` method on the root contract type if it -/// contains events. Expands to nothing otherwise. -fn expand_all_events(cx: &Context) -> TokenStream { - if cx.artifact.abi.events.is_empty() { - return quote! {}; - } - - let event_enum = expand_event_enum(cx); - let event_parse_log = expand_event_parse_log(cx); - - quote! { - impl Contract { - /// Returns a log stream with all events. - pub fn all_events(&self) -> self::ethcontract::dyns::DynAllEventsBuilder { - self::ethcontract::dyns::DynAllEventsBuilder::new( - self.raw_instance().web3(), - self.address(), - self.transaction_hash(), - ) - } - } - - #event_enum - #event_parse_log - } -} - -/// Expands into an enum with one variant for each distinct event type, -/// including anonymous types. -fn expand_event_enum(cx: &Context) -> TokenStream { - let variants = { - let mut events = cx.artifact.abi.events().collect::>(); - - // NOTE: We sort the events by name so that the generated enum is - // consistent. This also faciliates testing as so that the same ABI - // yields consistent code. - events.sort_unstable_by_key(|event| &event.name); - - events - .into_iter() - .map(|event| { - let struct_name = expand_struct_name(&event); - quote! { - #struct_name(self::event_data::#struct_name) - } - }) - .collect::>() - }; - - let derives = expand_derives(&cx.event_derives); - - quote! { - /// A contract event. - #[derive(Clone, Debug, Eq, PartialEq, #derives)] - pub enum Event { - #( #variants, )* - } - } -} - -/// Expands the `ParseLog` implementation for the event enum. -fn expand_event_parse_log(cx: &Context) -> TokenStream { - let all_events = { - let mut all_events = cx - .artifact - .abi - .events() - .map(|event| { - let struct_name = expand_struct_name(&event); - - let name = Literal::string(&event.name); - let decode_event = quote! { - log.clone().decode( - &Contract::artifact() - .abi - .event(#name) - .expect("generated event decode") - ) - }; - - (event, struct_name, decode_event) - }) - .collect::>(); - - // NOTE: We sort the events by name so that the anonymous error decoding - // is consistent. Since the events are stored in a `HashMap`, there is - // no guaranteed order, and in the case where there is ambiguity in - // decoding anonymous events, its nice if they follow some strict and - // predictable order. - all_events.sort_unstable_by_key(|(event, _, _)| &event.name); - all_events - }; - - let standard_event_match_arms = all_events - .iter() - .filter(|(event, _, _)| !event.anonymous) - .map(|(event, struct_name, decode_event)| { - // These are all possible stardard (i.e. non-anonymous) events that - // the contract can produce, along with its signature and index in - // the contract ABI. For these, we match topic 0 to the signature - // and try to decode. - - let signature = expand_hash(event.signature()); - quote! { - #signature => Ok(Event::#struct_name(#decode_event?)), - } - }) - .collect::>(); - - let anonymous_event_try_decode = all_events - .iter() - .filter(|(event, _, _)| event.anonymous) - .map(|(_, struct_name, decode_event)| { - // For anonymous events, just try to decode one at a time and return - // the first that succeeds. - - quote! { - if let Ok(data) = #decode_event { - return Ok(Event::#struct_name(data)); - } - } - }) - .collect::>(); - - let invalid_data = expand_invalid_data(); - - quote! { - impl self::ethcontract::contract::ParseLog for Event { - fn parse_log( - log: self::ethcontract::RawLog, - ) -> Result { - let standard_event = log.topics - .get(0) - .copied() - .map(|topic| match topic { - #( #standard_event_match_arms )* - _ => #invalid_data, - }); - - if let Some(Ok(data)) = standard_event { - return Ok(data); - } - - #( #anonymous_event_try_decode )* - - #invalid_data - } - } - } -} - -/// Expands an event property type. -/// -/// Note that this is slightly different than an expanding a Solidity type as -/// complex types like arrays and strings get emited as hashes when they are -/// indexed. -fn expand_input_type(input: &EventParam) -> Result { - Ok(match (&input.kind, input.indexed) { - (ParamType::Array(..), true) - | (ParamType::Bytes, true) - | (ParamType::FixedArray(..), true) - | (ParamType::String, true) - | (ParamType::Tuple(..), true) => { - quote! { self::ethcontract::H256 } - } - (kind, _) => types::expand(kind)?, - }) -} - -/// Expands a 256-bit `Hash` into a literal representation that can be used with -/// quasi-quoting for code generation. -fn expand_hash(hash: Hash) -> TokenStream { - let bytes = hash.as_bytes().iter().copied().map(Literal::u8_unsuffixed); - - quote! { - self::ethcontract::H256([#( #bytes ),*]) - } -} - -/// Expands to a generic `InvalidData` error. -fn expand_invalid_data() -> TokenStream { - quote! { - Err(self::ethcontract::errors::ExecutionError::from( - self::ethcontract::common::abi::Error::InvalidData - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ethcontract_common::abi::{EventParam, ParamType}; - - #[test] - fn expand_empty_filters() { - assert_quote!(expand_filters(&Context::default()).unwrap(), {}); - } - - #[test] - fn expand_transfer_filter() { - let event = Event { - name: "Transfer".into(), - inputs: vec![ - EventParam { - name: "from".into(), - kind: ParamType::Address, - indexed: true, - }, - EventParam { - name: "to".into(), - kind: ParamType::Address, - indexed: true, - }, - EventParam { - name: "amount".into(), - kind: ParamType::Uint(256), - indexed: false, - }, - ], - anonymous: false, - }; - let signature = expand_hash(event.signature()); - - assert_quote!(expand_filter(&event).unwrap(), { - /// Generated by `ethcontract`. - pub fn transfer(&self) -> self::event_builders::TransferBuilder { - self::event_builders::TransferBuilder( - self.instance.event(#signature) - .expect("generated event filter"), - ) - } - }); - } - - #[test] - fn expand_transfer_builder_topic_filters() { - let event = Event { - name: "Transfer".into(), - inputs: vec![ - EventParam { - name: "from".into(), - kind: ParamType::Address, - indexed: true, - }, - EventParam { - name: "to".into(), - kind: ParamType::Address, - indexed: true, - }, - EventParam { - name: "amount".into(), - kind: ParamType::Uint(256), - indexed: false, - }, - ], - anonymous: false, - }; - - #[rustfmt::skip] - assert_quote!(expand_builder_topic_filters(&event).unwrap(), { - #[doc = "Adds a filter for the from event parameter."] - pub fn from(mut self, topic: self::ethcontract::Topic) -> Self { - self.0 = (self.0).topic0(topic); - self - } - - #[doc = "Adds a filter for the to event parameter."] - pub fn to(mut self, topic: self::ethcontract::Topic) -> Self { - self.0 = (self.0).topic1(topic); - self - } - }); - } - - #[test] - fn expand_data_struct_value() { - let event = Event { - name: "Foo".into(), - inputs: vec![ - EventParam { - name: "a".into(), - kind: ParamType::Bool, - indexed: false, - }, - EventParam { - name: String::new(), - kind: ParamType::Address, - indexed: false, - }, - ], - anonymous: false, - }; - - let name = expand_struct_name(&event); - let params = expand_params(&event).unwrap(); - let (definition, construction) = expand_data_struct(&name, ¶ms); - - assert_quote!(definition, { - struct Foo { - pub a: bool, - pub p1: self::ethcontract::Address, - } - }); - assert_quote!(construction, { Foo { a, p1 } }); - } - - #[test] - fn expand_data_tuple_value() { - let event = Event { - name: "Foo".into(), - inputs: vec![ - EventParam { - name: String::new(), - kind: ParamType::Bool, - indexed: false, - }, - EventParam { - name: String::new(), - kind: ParamType::Address, - indexed: false, - }, - ], - anonymous: false, - }; - - let name = expand_struct_name(&event); - let params = expand_params(&event).unwrap(); - let (definition, construction) = expand_data_tuple(&name, ¶ms); - - assert_quote!(definition, { - struct Foo(pub bool, pub self::ethcontract::Address); - }); - assert_quote!(construction, { Foo(p0, p1) }); - } - - #[test] - fn expand_enum_for_all_events() { - let context = { - let mut context = Context::default(); - context.artifact.abi.events.insert( - "Foo".into(), - vec![Event { - name: "Foo".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Bool, - indexed: false, - }], - anonymous: false, - }], - ); - context.artifact.abi.events.insert( - "Bar".into(), - vec![Event { - name: "Bar".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Address, - indexed: false, - }], - anonymous: true, - }], - ); - context.event_derives = ["Asdf", "a::B", "a::b::c::D"] - .iter() - .map(|derive| syn::parse_str::(derive).unwrap()) - .collect(); - context - }; - - assert_quote!(expand_event_enum(&context), { - /// A contract event. - #[derive(Clone, Debug, Eq, PartialEq, Asdf, a::B, a::b::c::D)] - pub enum Event { - Bar(self::event_data::Bar), - Foo(self::event_data::Foo), - } - }); - } - - #[test] - fn expand_parse_log_impl_for_all_events() { - let context = { - let mut context = Context::default(); - context.artifact.abi.events.insert( - "Foo".into(), - vec![Event { - name: "Foo".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Bool, - indexed: false, - }], - anonymous: false, - }], - ); - context.artifact.abi.events.insert( - "Bar".into(), - vec![Event { - name: "Bar".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Address, - indexed: false, - }], - anonymous: true, - }], - ); - context - }; - - let foo_signature = expand_hash(context.artifact.abi.event("Foo").unwrap().signature()); - let invalid_data = expand_invalid_data(); - - assert_quote!(expand_event_parse_log(&context), { - impl self::ethcontract::contract::ParseLog for Event { - fn parse_log( - log: self::ethcontract::RawLog, - ) -> Result { - let standard_event = log.topics - .get(0) - .copied() - .map(|topic| match topic { - #foo_signature => Ok(Event::Foo( - log.clone().decode( - &Contract::artifact() - .abi - .event("Foo") - .expect("generated event decode") - )? - )), - _ => #invalid_data, - }); - - if let Some(Ok(data)) = standard_event { - return Ok(data); - } - - if let Ok(data) = log.clone().decode( - &Contract::artifact() - .abi - .event("Bar") - .expect("generated event decode") - ) { - return Ok(Event::Bar(data)); - } - - #invalid_data - } - } - }); - } - - #[test] - #[rustfmt::skip] - fn expand_hash_value() { - assert_quote!( - expand_hash( - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".parse().unwrap() - ), - { - self::ethcontract::H256([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 - ]) - }, - ); - } -} diff --git a/crates/ethers-derive/src/src/contract/methods.rs b/crates/ethers-derive/src/src/contract/methods.rs deleted file mode 100644 index d1a037c6..00000000 --- a/crates/ethers-derive/src/src/contract/methods.rs +++ /dev/null @@ -1,255 +0,0 @@ -use crate::contract::{types, Context}; -use crate::util; -use anyhow::{anyhow, Context as _, Result}; -use ethcontract_common::abi::{Function, Param}; -use ethcontract_common::abiext::FunctionExt; -use ethcontract_common::hash::H32; -use inflector::Inflector; -use proc_macro2::{Literal, TokenStream}; -use quote::quote; -use syn::Ident; - -pub(crate) fn expand(cx: &Context) -> Result { - let functions = expand_functions(cx)?; - let fallback = expand_fallback(cx); - - Ok(quote! { - #functions - #fallback - }) -} - -/// Expands a context into a method struct containing all the generated bindings -/// to the Solidity contract methods. -fn expand_functions(cx: &Context) -> Result { - let mut aliases = cx.method_aliases.clone(); - let functions = cx - .artifact - .abi - .functions() - .map(|function| { - let signature = function.abi_signature(); - expand_function(&cx, function, aliases.remove(&signature)) - .with_context(|| format!("error expanding function '{}'", signature)) - }) - .collect::>>()?; - if let Some(unused) = aliases.keys().next() { - return Err(anyhow!( - "a manual method alias for '{}' was specified but this method does not exist", - unused, - )); - } - - let methods_attrs = quote! { #[derive(Clone)] }; - let methods_struct = quote! { - struct Methods { - instance: self::ethcontract::dyns::DynInstance, - } - }; - - if functions.is_empty() { - // NOTE: The methods struct is still needed when there are no functions - // as it contains the the runtime instance. The code is setup this way - // so that the contract can implement `Deref` targetting the methods - // struct and, therefore, call the methods directly. - return Ok(quote! { - #methods_attrs - #methods_struct - }); - } - - Ok(quote! { - impl Contract { - /// Retrives a reference to type containing all the generated - /// contract methods. This can be used for methods where the name - /// would collide with a common method (like `at` or `deployed`). - pub fn methods(&self) -> &Methods { - &self.methods - } - } - - /// Type containing all contract methods for generated contract type. - #methods_attrs - pub #methods_struct - - #[allow(clippy::too_many_arguments, clippy::type_complexity)] - impl Methods { - #( #functions )* - } - - impl std::ops::Deref for Contract { - type Target = Methods; - fn deref(&self) -> &Self::Target { - &self.methods - } - } - }) -} - -fn expand_function(cx: &Context, function: &Function, alias: Option) -> Result { - let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); - let signature = function.abi_signature(); - let selector = expand_selector(function.selector()); - - let doc_str = cx - .artifact - .devdoc - .methods - .get(&signature) - .or_else(|| cx.artifact.userdoc.methods.get(&signature)) - .and_then(|entry| entry.details.as_ref()) - .map(String::as_str) - .unwrap_or("Generated by `ethcontract`"); - let doc = util::expand_doc(doc_str); - - let input = expand_inputs(&function.inputs)?; - let outputs = expand_fn_outputs(&function.outputs)?; - let (method, result_type_name) = if function.constant { - (quote! { view_method }, quote! { DynViewMethodBuilder }) - } else { - (quote! { method }, quote! { DynMethodBuilder }) - }; - let result = quote! { self::ethcontract::dyns::#result_type_name<#outputs> }; - let arg = expand_inputs_call_arg(&function.inputs); - - Ok(quote! { - #doc - pub fn #name(&self #input) -> #result { - self.instance.#method(#selector, #arg) - .expect("generated call") - } - }) -} - -pub(crate) fn expand_inputs(inputs: &[Param]) -> Result { - let params = inputs - .iter() - .enumerate() - .map(|(i, param)| { - let name = util::expand_input_name(i, ¶m.name); - let kind = types::expand(¶m.kind)?; - Ok(quote! { #name: #kind }) - }) - .collect::>>()?; - Ok(quote! { #( , #params )* }) -} - -pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { - let names = inputs - .iter() - .enumerate() - .map(|(i, param)| util::expand_input_name(i, ¶m.name)); - quote! { ( #( #names ,)* ) } -} - -fn expand_fn_outputs(outputs: &[Param]) -> Result { - match outputs.len() { - 0 => Ok(quote! { self::ethcontract::Void }), - 1 => types::expand(&outputs[0].kind), - _ => { - let types = outputs - .iter() - .map(|param| types::expand(¶m.kind)) - .collect::>>()?; - Ok(quote! { (#( #types ),*) }) - } - } -} - -fn expand_selector(selector: H32) -> TokenStream { - let bytes = selector.iter().copied().map(Literal::u8_unsuffixed); - quote! { [#( #bytes ),*] } -} - -/// Expands a context into fallback method when the contract implements one, -/// and an empty token stream otherwise. -fn expand_fallback(cx: &Context) -> TokenStream { - if cx.artifact.abi.fallback { - quote! { - impl Contract { - /// Returns a method builder to setup a call to a smart - /// contract's fallback function. - pub fn fallback(&self, data: D) -> self::ethcontract::dyns::DynMethodBuilder< - self::ethcontract::Void, - > - where - D: Into>, - { - self.raw_instance().fallback(data) - .expect("generated fallback method") - } - } - } - } else { - quote! {} - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ethcontract_common::abi::ParamType; - - #[test] - fn expand_inputs_empty() { - assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},); - } - - #[test] - fn expand_inputs_() { - assert_quote!( - expand_inputs( - - &[ - Param { - name: "a".to_string(), - kind: ParamType::Bool, - }, - Param { - name: "b".to_string(), - kind: ParamType::Address, - }, - ], - ) - .unwrap(), - { , a: bool, b: self::ethcontract::Address }, - ); - } - - #[test] - fn expand_fn_outputs_empty() { - assert_quote!(expand_fn_outputs(&[],).unwrap(), { - self::ethcontract::Void - }); - } - - #[test] - fn expand_fn_outputs_single() { - assert_quote!( - expand_fn_outputs(&[Param { - name: "a".to_string(), - kind: ParamType::Bool, - }]) - .unwrap(), - { bool }, - ); - } - - #[test] - fn expand_fn_outputs_muliple() { - assert_quote!( - expand_fn_outputs(&[ - Param { - name: "a".to_string(), - kind: ParamType::Bool, - }, - Param { - name: "b".to_string(), - kind: ParamType::Address, - }, - ],) - .unwrap(), - { (bool, self::ethcontract::Address) }, - ); - } -} diff --git a/crates/ethers-types/src/log.rs b/crates/ethers-types/src/log.rs index f812dbab..e0fd9127 100644 --- a/crates/ethers-types/src/log.rs +++ b/crates/ethers-types/src/log.rs @@ -89,7 +89,9 @@ pub struct Filter { impl Filter { pub fn new() -> Self { - Self::default() + let filter = Self::default(); + // filter.topics = vec![H256::zero().into(); 4]; + filter } pub fn from_block>(mut self, block: T) -> Self { diff --git a/crates/ethers/Cargo.toml b/crates/ethers/Cargo.toml index 808e2c60..6c97c098 100644 --- a/crates/ethers/Cargo.toml +++ b/crates/ethers/Cargo.toml @@ -24,13 +24,15 @@ utils = ["ethers-utils"] [dependencies] ethers-abi = { path = "../ethers-abi", optional = true } -ethers-contract = { path = "../ethers-contract", optional = true } +ethers-contract = { path = "../ethers-contract", features = ["abigen"], optional = true } ethers-providers = { path = "../ethers-providers", optional = true } ethers-signers = { path = "../ethers-signers", optional = true } ethers-types = { path = "../ethers-types", optional = true } ethers-utils = { path = "../ethers-utils", optional = true } [dev-dependencies] +ethers-contract = { path = "../ethers-contract", features = ["abigen"] } + anyhow = "1.0.31" tokio = { version = "0.2.21", features = ["macros"] } serde_json = "1.0.53" diff --git a/crates/ethers/examples/contract.rs b/crates/ethers/examples/contract.rs index c6f4ce69..d02c0c52 100644 --- a/crates/ethers/examples/contract.rs +++ b/crates/ethers/examples/contract.rs @@ -1,68 +1,15 @@ -use ethers::{ - abi::{Detokenize, InvalidOutputType, Token}, - contract::{Contract, Event, Sender}, - providers::{HttpProvider, JsonRpcClient}, - signers::{Client, MainnetWallet, Signer}, - types::{Address, H256}, -}; - use anyhow::Result; -use serde::Serialize; +use ethers::{providers::HttpProvider, signers::MainnetWallet, types::Address}; use std::convert::TryFrom; -const ABI: &'static str = r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#; +use ethers::contract::abigen; -// abigen!(SimpleContract, ABI); - -#[derive(Clone, Debug, Serialize)] -// TODO: This should be `derive`-able on such types -> similar to how Zexe's Deserialize is done -struct ValueChanged { - author: Address, - old_value: String, - new_value: String, -} - -impl Detokenize for ValueChanged { - fn from_tokens(tokens: Vec) -> Result { - let author: Address = tokens[0].clone().to_address().unwrap(); - let old_value = tokens[1].clone().to_string().unwrap(); - let new_value = tokens[2].clone().to_string().unwrap(); - - Ok(Self { - author, - old_value, - new_value, - }) - } -} - -struct SimpleContract<'a, S, P>(Contract<'a, S, P>); - -impl<'a, S: Signer, P: JsonRpcClient> SimpleContract<'a, S, P> { - fn new>(address: T, client: &'a Client<'a, S, P>) -> Self { - let contract = Contract::new(client, serde_json::from_str(&ABI).unwrap(), address.into()); - Self(contract) - } - - fn set_value>(&self, val: T) -> Sender<'a, S, P, H256> { - self.0 - .method("setValue", Some(val.into())) - .expect("method not found (this should never happen)") - } - - fn value_changed<'b>(&'a self) -> Event<'a, 'b, P, ValueChanged> - where - 'a: 'b, - { - self.0.event("ValueChanged").expect("event does not exist") - } - - fn get_value(&self) -> Sender<'a, S, P, String> { - self.0 - .method("getValue", None::<()>) - .expect("method not found (this should never happen)") - } -} +// Generate the contract code +abigen!( + SimpleContract, + r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","constant": true, "type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#, + event_derives(serde::Deserialize, serde::Serialize) +); #[tokio::main] async fn main() -> Result<()> { @@ -83,7 +30,7 @@ async fn main() -> Result<()> { let contract = SimpleContract::new(addr, &client); // call the method - let _tx_hash = contract.set_value("hi").send().await?; + let _tx_hash = contract.set_value("hi".to_owned()).send().await?; let logs = contract.value_changed().from_block(0u64).query().await?;