diff --git a/Cargo.lock b/Cargo.lock index 8a3f692..5425507 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,9 +77,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d1aecf3cab3d0e7383064ce488616434b4ade10d8904dff422e74203c712f" +checksum = "5ecf116474faea3e30ecb03cb14548598ca8243d5316ce50f820e67b3e848473" dependencies = [ "alloy-consensus", "alloy-contract", @@ -88,6 +88,7 @@ dependencies = [ "alloy-genesis", "alloy-json-rpc", "alloy-network", + "alloy-node-bindings", "alloy-provider", "alloy-rpc-client", "alloy-rpc-types", @@ -114,9 +115,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c6ad411efe0f49e0e99b9c7d8749a1eb55f6dbf74a1bc6953ab285b02c4f67" +checksum = "1b6093bc69509849435a2d68237a2e9fea79d27390c8e62f1e4012c460aabad8" dependencies = [ "alloy-eips", "alloy-primitives", @@ -139,9 +140,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf397edad57b696501702d5887e4e14d7d0bbae9fbb6439e148d361f7254f45" +checksum = "8d1cfed4fefd13b5620cb81cdb6ba397866ff0de514c1b24806e6e79cdff5570" dependencies = [ "alloy-consensus", "alloy-eips", @@ -153,9 +154,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977b97d271159578afcb26e39e1ca5ce1a7f937697793d7d571b0166dd8b8225" +checksum = "f28074a21cd4f7c3a7ab218c4f38fae6be73944e1feae3b670c68b60bf85ca40" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -241,9 +242,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749b8449e4daf7359bdf1dabdba6ce424ff8b1bdc23bdb795661b2e991a08d87" +checksum = "5937e2d544e9b71000942d875cbc57965b32859a666ea543cc57aae5a06d602d" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -261,9 +262,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fcbae2107f3f2df2b02bb7d9e81e8aa730ae371ca9dd7fd0c81c3d0cb78a452" +checksum = "c51b4c13e02a8104170a4de02ccf006d7c233e6c10ab290ee16e7041e6ac221d" dependencies = [ "alloy-eips", "alloy-primitives", @@ -273,6 +274,19 @@ dependencies = [ "serde_with", ] +[[package]] +name = "alloy-hardforks" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819a3620fe125e0fff365363315ee5e24c23169173b19747dfd6deba33db8990" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", +] + [[package]] name = "alloy-json-abi" version = "1.2.1" @@ -287,9 +301,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc30b0e20fcd0843834ecad2a716661c7b9d5aca2486f8e96b93d5246eb83e06" +checksum = "b590caa6b6d8bc10e6e7a7696c59b1e550e89f27f50d1ee13071150d3a3e3f66" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -302,9 +316,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaeb681024cf71f5ca14f3d812c0a8d8b49f13f7124713538e66d74d3bfe6aff" +checksum = "36fe5af1fca03277daa56ad4ce5f6d623d3f4c2273ea30b9ee8674d18cefc1fa" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -328,9 +342,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03ad273e1c55cc481889b4130e82860e33624e6969e9a08854e0f3ebe659295" +checksum = "793df1e3457573877fbde8872e4906638fde565ee2d3bd16d04aad17d43dbf0e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -339,6 +353,27 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-node-bindings" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de75f0d0af3c6cb0bd3648e530289b2c542b7bf57e7d4296d1c29281418a476" +dependencies = [ + "alloy-genesis", + "alloy-hardforks", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "alloy-signer-local", + "k256", + "rand 0.8.5", + "serde_json", + "tempfile", + "thiserror 2.0.12", + "tracing", + "url", +] + [[package]] name = "alloy-primitives" version = "1.2.1" @@ -360,7 +395,7 @@ dependencies = [ "proptest", "rand 0.9.1", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -368,9 +403,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc164acf8c41c756e76c7aea3be8f0fb03f8a3ef90a33e3ddcea5d1614d8779" +checksum = "d59879a772ebdcde9dc4eb38b2535d32e8503d3175687cc09e763a625c5fcf32" dependencies = [ "alloy-chains", "alloy-consensus", @@ -378,8 +413,10 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", + "alloy-node-bindings", "alloy-primitives", "alloy-rpc-client", + "alloy-rpc-types-anvil", "alloy-rpc-types-eth", "alloy-signer", "alloy-sol-types", @@ -430,15 +467,14 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c44d31bcb9afad460915fe1fba004a2af5a07a3376c307b9bdfeec3678c209" +checksum = "7f060e3bb9f319eb01867a2d6d1ff9e0114e8877f5ca8f5db447724136106cae" dependencies = [ "alloy-json-rpc", "alloy-primitives", "alloy-transport", "alloy-transport-http", - "async-stream", "futures", "pin-project 1.1.10", "reqwest", @@ -448,16 +484,27 @@ dependencies = [ "tokio-stream", "tower", "tracing", - "tracing-futures", "url", "wasmtimer", ] [[package]] name = "alloy-rpc-types" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ba2cf3d3c6ece87f1c6bb88324a997f28cf0ad7e98d5e0b6fa91c4003c30916" +checksum = "d47b637369245d2dafef84b223b1ff5ea59e6cd3a98d2d3516e32788a0b216df" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-anvil" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b1f499acb3fc729615147bc113b8b798b17379f19d43058a687edc5792c102" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -467,9 +514,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5b22062142ce3b2ed3374337d4b343437e5de6959397f55d2c9fe2c2ce0162" +checksum = "1e26b4dd90b33bd158975307fb9cf5fafa737a0e33cbb772a8648bf8be13c104" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -478,9 +525,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391e59f81bacbffc7bddd2da3a26d6eec0e2058e9237c279e9b1052bdf21b49e" +checksum = "46586ec3c278639fc0e129f0eb73dbfa3d57f683c44b2ff5e066fab7ba63fa1f" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -493,14 +540,15 @@ dependencies = [ "itertools 0.14.0", "serde", "serde_json", + "serde_with", "thiserror 2.0.12", ] [[package]] name = "alloy-serde" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea08bc854235d4dff08fd57df8033285c11b8d7548b20c6da218194e7e6035f" +checksum = "1e1722bc30feef87cc0fa824e43c9013f9639cc6c037be7be28a31361c788be2" dependencies = [ "alloy-primitives", "serde", @@ -509,9 +557,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb3759f85ef5f010a874d9ebd5ee6ce01cac65211510863124e0ebac6552db0" +checksum = "d3674beb29e68fbbc7be302b611cf35fe07b736e308012a280861df5a2361395" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -526,9 +574,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7942b850ec7be43de89b2680321d7921b7620b25be53b9981aae6fb29daa9e97" +checksum = "605b1659b320b16708bb84b41038b2f0e2a60d90972c28319c4f5a4866f0efd4" dependencies = [ "alloy-consensus", "alloy-network", @@ -544,9 +592,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74809e45053bd43d24338e618202ebea68d5660aa9632d77b0244faa2dcaa9d1" +checksum = "0a207671ef0bf6f61e9c80c9ccb6d203439071252fb35886d6a89aae5431cd9c" dependencies = [ "alloy-consensus", "alloy-network", @@ -562,9 +610,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c7e67367bc2b1d5790236448d2402865a4f0bc2b53cfda06d71b7ba3dbdffd" +checksum = "cce5e8c97a526d39052035a99652b9cfacf0d646d4a3625fac9c919d20a46fb0" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -582,9 +630,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d95902d29e1290809e1c967a1e974145b44b78f6e3e12fc07a60c1225e3df0" +checksum = "ad7094c39cd41b03ed642145b0bd37251e31a9cf2ed19e1ce761f089867356a6" dependencies = [ "alloy-consensus", "alloy-network", @@ -671,9 +719,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdf4b7fc58ebb2605b2fc5a33dae5cf15527ea70476978351cc0db1c596ea93" +checksum = "f89bec2f59a41c0e259b6fe92f78dfc49862c17d10f938db9c33150d5a7f42b6" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -694,9 +742,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4b0f3a9c28bcd3761504d9eb3578838d6d115c8959fc1ea05f59a3a8f691af" +checksum = "0d3615ec64d775fec840f4e9d5c8e1f739eb1854d8d28db093fb3d4805e0cb53" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -725,9 +773,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79bf2869e66904b2148c809e7a75e23ca26f5d7b46663a149a1444fb98a69d1d" +checksum = "9f916ff6d52f219c44a9684aea764ce2c7e1d53bd4a724c9b127863aeacc30bb" dependencies = [ "alloy-primitives", "darling", @@ -994,11 +1042,52 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-arn" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da0fae6942d45915eebb3c2cfad48c5359d969a58b24696c71baf6a54489a98" +dependencies = [ + "lazy_static", + "regex", + "serde", +] + +[[package]] +name = "aws-config" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd9b83179adf8998576317ce47785948bcff399ec5b15f4dfbdedd44ddf5b92" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.3.1", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + [[package]] name = "aws-credential-types" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465" +checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1006,11 +1095,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "aws-runtime" -version = "1.5.8" +version = "1.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2" +checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -1032,9 +1144,53 @@ dependencies = [ [[package]] name = "aws-sdk-kms" +version = "1.79.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5603bd5e0487e90acdef4a9be019f55c841e8eb72d3cb2e88c1c112c67a59db" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.76.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bf26698dd6d238ef1486bdda46f22a589dc813368ba868dc3d94c8d27b56ba" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" version = "1.77.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cd57d0c1a5bd6c7eaa2b26462e046d5ca7b72189346718d2435dfc48bfa988b" +checksum = "09cd07ed1edd939fae854a22054299ae3576500f4e0fadc560ca44f9c6ea1664" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1052,6 +1208,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-sts" +version = "1.78.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37f7766d2344f56d10d12f3c32993da36d78217f32594fe4fb8e57a538c1cdea" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sigv4" version = "1.3.3" @@ -1105,6 +1284,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-http-client" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.11", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.6.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.28", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tower", + "tracing", +] + [[package]] name = "aws-smithy-json" version = "0.61.4" @@ -1123,14 +1331,25 @@ dependencies = [ "aws-smithy-runtime-api", ] +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + [[package]] name = "aws-smithy-runtime" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e" +checksum = "c3aaec682eb189e43c8a19c3dab2fe54590ad5f2cc2d26ab27608a20f2acf81c" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-types", @@ -1148,9 +1367,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8531b6d8882fd8f48f82a9754e682e29dd44cff27154af51fa3eb730f59efb" +checksum = "38280ac228bc479f347fcfccf4bf4d22d68f3bb4629685cb591cabd856567bbc" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1172,6 +1391,7 @@ dependencies = [ "base64-simd", "bytes", "bytes-utils", + "futures-core", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", @@ -1184,6 +1404,17 @@ dependencies = [ "ryu", "serde", "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +dependencies = [ + "xmlparser", ] [[package]] @@ -1214,7 +1445,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "itoa", "matchit", @@ -1344,6 +1575,29 @@ dependencies = [ "virtue", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.104", + "which", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1487,9 +1741,20 @@ version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -1579,6 +1844,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.40" @@ -1604,6 +1880,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "coins-ledger" version = "0.12.0" @@ -2159,6 +2444,10 @@ name = "engine-core" version = "0.1.0" dependencies = [ "alloy", + "alloy-signer-aws", + "aws-config", + "aws-credential-types", + "aws-sdk-kms", "engine-aa-types", "schemars 0.8.22", "serde", @@ -2184,9 +2473,11 @@ dependencies = [ "rand 0.9.1", "serde", "serde_json", + "serde_repr", "thiserror 2.0.12", "tokio", "tracing", + "vault-types", ] [[package]] @@ -2354,6 +2645,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2465,7 +2762,7 @@ dependencies = [ "bytes", "chrono", "futures", - "hyper", + "hyper 1.6.0", "jsonwebtoken", "once_cell", "prost", @@ -2558,6 +2855,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.11" @@ -2680,6 +2996,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "0.2.12" @@ -2748,6 +3073,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -2757,7 +3106,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2769,6 +3118,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -2776,13 +3141,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper", + "hyper 1.6.0", "hyper-util", - "rustls", - "rustls-native-certs", + "rustls 0.23.28", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots", ] @@ -2793,7 +3158,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -2808,7 +3173,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -2829,7 +3194,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper", + "hyper 1.6.0", "ipnet", "libc", "percent-encoding", @@ -3090,6 +3455,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -3165,12 +3540,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] + [[package]] name = "libm" version = "0.2.15" @@ -3189,6 +3580,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -3302,6 +3699,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3383,6 +3786,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3840,6 +4253,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +dependencies = [ + "proc-macro2", + "syn 2.0.104", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3960,8 +4383,8 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", - "rustls", + "rustc-hash 2.1.1", + "rustls 0.23.28", "socket2", "thiserror 2.0.12", "tokio", @@ -3980,8 +4403,8 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", - "rustls", + "rustc-hash 2.1.1", + "rustls 0.23.28", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -4232,12 +4655,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.6.0", + "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", "js-sys", @@ -4248,8 +4671,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-native-certs", + "rustls 0.23.28", + "rustls-native-certs 0.8.1", "rustls-pki-types", "serde", "serde_json", @@ -4257,7 +4680,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower", "tower-http", @@ -4375,6 +4798,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4405,6 +4834,19 @@ dependencies = [ "semver 1.0.26", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -4414,25 +4856,50 @@ dependencies = [ "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -4445,6 +4912,15 @@ dependencies = [ "security-framework 3.2.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -4455,12 +4931,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4569,6 +5056,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.7.3" @@ -4753,6 +5250,17 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -5076,7 +5584,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -5104,6 +5612,7 @@ dependencies = [ "aide", "alloy", "anyhow", + "aws-arn", "axum", "config", "engine-aa-core", @@ -5303,13 +5812,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.28", "tokio", ] @@ -5382,20 +5901,20 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-timeout", "hyper-util", "percent-encoding", "pin-project 1.1.10", "prost", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "socket2", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-stream", "tower", "tower-layer", @@ -5498,18 +6017,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "futures", - "futures-task", - "pin-project 1.1.10", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -5663,6 +6170,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5965,6 +6478,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6318,6 +6843,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yaml-rust2" version = "0.10.3" diff --git a/Cargo.toml b/Cargo.toml index 72ea61b..c2593ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,6 @@ members = [ resolver = "2" [workspace.dependencies] -alloy = { version = "1.0.8" } +alloy = { version = "1.0.23" } vault-types = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "pb/update-alloy" } vault-sdk = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "pb/update-alloy" } diff --git a/aa-types/src/userop.rs b/aa-types/src/userop.rs index 7e51133..16a9d46 100644 --- a/aa-types/src/userop.rs +++ b/aa-types/src/userop.rs @@ -1,6 +1,6 @@ use alloy::{ core::sol_types::SolValue, - primitives::{Address, B256, Bytes, ChainId, U256, keccak256}, + primitives::{Address, B256, Bytes, ChainId, U256, address, keccak256}, rpc::types::{PackedUserOperation, UserOperation}, }; use serde::{Deserialize, Serialize}; @@ -13,6 +13,9 @@ pub enum VersionedUserOp { V0_7(PackedUserOperation), } +pub const ENTRYPOINT_ADDRESS_V0_6: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); // v0.6 +pub const ENTRYPOINT_ADDRESS_V0_7: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); // v0.7 + /// Error type for UserOp operations #[derive( Debug, @@ -182,3 +185,34 @@ pub fn compute_user_op_v07_hash( let final_hash = keccak256(&outer_encoded); Ok(final_hash) } + +impl VersionedUserOp { + pub fn hash(&self, chain_id: ChainId) -> Result { + match self { + VersionedUserOp::V0_6(op) => { + compute_user_op_v06_hash(op, ENTRYPOINT_ADDRESS_V0_6, chain_id) + } + VersionedUserOp::V0_7(op) => { + compute_user_op_v07_hash(op, ENTRYPOINT_ADDRESS_V0_7, chain_id) + } + } + } + + pub fn hash_with_custom_entrypoint( + &self, + chain_id: ChainId, + entrypoint: Address, + ) -> Result { + match self { + VersionedUserOp::V0_6(op) => compute_user_op_v06_hash(op, entrypoint, chain_id), + VersionedUserOp::V0_7(op) => compute_user_op_v07_hash(op, entrypoint, chain_id), + } + } + + pub fn default_entrypoint(&self) -> Address { + match self { + VersionedUserOp::V0_6(_) => ENTRYPOINT_ADDRESS_V0_6, + VersionedUserOp::V0_7(_) => ENTRYPOINT_ADDRESS_V0_7, + } + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 5427590..5594326 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -19,3 +19,7 @@ thirdweb-core = { version = "0.1.0", path = "../thirdweb-core" } uuid = { version = "1.17.0", features = ["v4"] } utoipa = { version = "5.4.0", features = ["preserve_order"] } serde_with = "3.13.0" +alloy-signer-aws = { version = "1.0.23", features = ["eip712"] } +aws-config = "1.8.2" +aws-sdk-kms = "1.79.0" +aws-credential-types = "1.2.4" diff --git a/core/src/constants.rs b/core/src/constants.rs index bb799c6..87d9cc9 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -11,7 +11,3 @@ pub const DEFAULT_FACTORY_ADDRESS_V0_6: Address = pub const DEFAULT_IMPLEMENTATION_ADDRESS_V0_6: Address = address!("0xf22175c80c6e074C171811C59C6c0087e2a6a346"); - -pub const ENTRYPOINT_ADDRESS_V0_6: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); // v0.6 - -pub const ENTRYPOINT_ADDRESS_V0_7: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); // v0.7 diff --git a/core/src/credentials.rs b/core/src/credentials.rs index ff311fd..8871d8b 100644 --- a/core/src/credentials.rs +++ b/core/src/credentials.rs @@ -1,13 +1,71 @@ +use alloy::primitives::ChainId; +use alloy::signers::local::PrivateKeySigner; +use alloy_signer_aws::AwsSigner; +use aws_config::{BehaviorVersion, Region}; +use aws_credential_types::provider::future::ProvideCredentials as ProvideCredentialsFuture; +use aws_sdk_kms::config::{Credentials, ProvideCredentials}; use serde::{Deserialize, Serialize}; use thirdweb_core::auth::ThirdwebAuth; use thirdweb_core::iaw::AuthToken; -use vault_types::enclave::auth::Auth; +use vault_types::enclave::auth::Auth as VaultAuth; + +use crate::error::EngineError; + +impl SigningCredential { + /// Create a random private key credential for testing + pub fn random_local() -> Self { + SigningCredential::PrivateKey(PrivateKeySigner::random()) + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SigningCredential { - Vault(Auth), - Iaw { - auth_token: AuthToken, - thirdweb_auth: ThirdwebAuth + Vault(VaultAuth), + Iaw { + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, }, + AwsKms(AwsKmsCredential), + /// Private key signer for testing and development + /// Note: This should only be used in test environments + #[serde(skip)] + PrivateKey(PrivateKeySigner), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AwsKmsCredential { + pub access_key_id: String, + pub secret_access_key: String, + pub key_id: String, + pub region: String, +} + +impl ProvideCredentials for AwsKmsCredential { + fn provide_credentials<'a>(&'a self) -> ProvideCredentialsFuture<'a> + where + Self: 'a, + { + let credentials = Credentials::new( + self.access_key_id.clone(), + self.secret_access_key.clone(), + None, + None, + "engine-core", + ); + ProvideCredentialsFuture::ready(Ok(credentials)) + } +} + +impl AwsKmsCredential { + pub async fn get_signer(&self, chain_id: Option) -> Result { + let config = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(self.clone()) + .region(Region::new(self.region.clone())) + .load() + .await; + let client = aws_sdk_kms::Client::new(&config); + + let signer = AwsSigner::new(client, self.key_id.clone(), chain_id).await?; + Ok(signer) + } } diff --git a/core/src/error.rs b/core/src/error.rs index d3ef448..ae808f0 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::defs::AddressDef; use alloy::{ primitives::Address, @@ -5,6 +7,8 @@ use alloy::{ RpcError as AlloyRpcError, TransportErrorKind, http::reqwest::header::InvalidHeaderValue, }, }; +use alloy_signer_aws::AwsSignerError; +use aws_sdk_kms::error::SdkError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thirdweb_core::error::ThirdwebError; @@ -242,11 +246,150 @@ pub enum EngineError { #[error("Thirdweb error: {message}")] ThirdwebError { message: String }, + #[schema(title = "AWS KMS Error")] + #[error(transparent)] + #[serde(rename_all = "camelCase")] + AwsKmsSignerError { + #[serde(flatten)] + error: SerialisableAwsSignerError, + }, + #[schema(title = "Engine Internal Error")] #[error("Internal error: {message}")] InternalError { message: String }, } +#[derive(thiserror::Error, Debug, Serialize, Clone, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] +pub enum SerialisableAwsSdkError { + /// The request failed during construction. It was not dispatched over the network. + #[error("Construction failure: {message}")] + ConstructionFailure { message: String }, + + /// The request failed due to a timeout. The request MAY have been sent and received. + #[error("Timeout error: {message}")] + TimeoutError { message: String }, + + /// The request failed during dispatch. An HTTP response was not received. The request MAY + /// have been sent. + #[error("Dispatch failure: {message}")] + DispatchFailure { message: String }, + + /// A response was received but it was not parseable according the the protocol (for example + /// the server hung up without sending a complete response) + #[error("Response error: {message}")] + ResponseError { message: String }, + + /// An error response was received from the service + #[error("Service error: {message}")] + ServiceError { message: String }, + + #[error("Other error: {message}")] + Other { message: String }, +} + +#[derive(Error, Debug, Serialize, Clone, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] +pub enum SerialisableAwsSignerError { + /// Thrown when the AWS KMS API returns a signing error. + #[error(transparent)] + Sign { + aws_sdk_error: SerialisableAwsSdkError, + }, + + /// Thrown when the AWS KMS API returns an error. + #[error(transparent)] + GetPublicKey { + aws_sdk_error: SerialisableAwsSdkError, + }, + + /// [`ecdsa`] error. + #[error("ECDSA error: {message}")] + K256 { message: String }, + + /// [`spki`] error. + #[error("SPKI error: {message}")] + Spki { message: String }, + + /// [`hex`](mod@hex) error. + #[error("Hex error: {message}")] + Hex { message: String }, + + /// Thrown when the AWS KMS API returns a response without a signature. + #[error("signature not found in response")] + SignatureNotFound, + + /// Thrown when the AWS KMS API returns a response without a public key. + #[error("public key not found in response")] + PublicKeyNotFound, + + #[error("Unknown error: {message}")] + Unknown { message: String }, +} + +impl From> for SerialisableAwsSdkError { + fn from(err: SdkError) -> Self { + match err { + SdkError::ConstructionFailure(err) => SerialisableAwsSdkError::ConstructionFailure { + message: format!("{:?}", err), + }, + SdkError::TimeoutError(err) => SerialisableAwsSdkError::TimeoutError { + message: format!("{:?}", err), + }, + SdkError::DispatchFailure(err) => SerialisableAwsSdkError::DispatchFailure { + message: format!("{:?}", err), + }, + SdkError::ResponseError(err) => SerialisableAwsSdkError::ResponseError { + message: format!("{:?}", err), + }, + SdkError::ServiceError(err) => SerialisableAwsSdkError::ServiceError { + message: format!("{:?}", err), + }, + _ => SerialisableAwsSdkError::Other { + message: format!("{:?}", err), + }, + } + } +} + +impl From for EngineError { + fn from(err: AwsSignerError) -> Self { + match err { + AwsSignerError::Sign(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: err.into(), + }, + }, + AwsSignerError::GetPublicKey(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::GetPublicKey { + aws_sdk_error: err.into(), + }, + }, + AwsSignerError::K256(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::K256 { + message: err.to_string(), + }, + }, + AwsSignerError::Spki(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Spki { + message: err.to_string(), + }, + }, + AwsSignerError::Hex(err) => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Hex { + message: err.to_string(), + }, + }, + AwsSignerError::SignatureNotFound => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::SignatureNotFound, + }, + AwsSignerError::PublicKeyNotFound => EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::PublicKeyNotFound, + }, + } + } +} + impl From for EngineError { fn from(err: vault_sdk::error::VaultError) -> Self { let message = match &err { diff --git a/core/src/execution_options/aa.rs b/core/src/execution_options/aa.rs index fb894a4..7820d52 100644 --- a/core/src/execution_options/aa.rs +++ b/core/src/execution_options/aa.rs @@ -1,13 +1,13 @@ -use crate::{ - constants::{DEFAULT_FACTORY_ADDRESS_V0_6, ENTRYPOINT_ADDRESS_V0_6}, - defs::AddressDef, - error::EngineError, +use crate::{constants::DEFAULT_FACTORY_ADDRESS_V0_6, defs::AddressDef, error::EngineError}; +use alloy::{ + hex::FromHex, + primitives::{Address, Bytes}, }; -use alloy::{hex::FromHex, primitives::{Address, Bytes}}; +use engine_aa_types::{ENTRYPOINT_ADDRESS_V0_6, ENTRYPOINT_ADDRESS_V0_7}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -use crate::constants::{DEFAULT_FACTORY_ADDRESS_V0_7, ENTRYPOINT_ADDRESS_V0_7}; +use crate::constants::DEFAULT_FACTORY_ADDRESS_V0_7; #[derive(Deserialize, Serialize, Debug, JsonSchema, Clone, Copy, utoipa::ToSchema)] pub enum EntrypointVersion { diff --git a/core/src/signer.rs b/core/src/signer.rs index 4698a19..a8948ca 100644 --- a/core/src/signer.rs +++ b/core/src/signer.rs @@ -2,9 +2,11 @@ use alloy::{ consensus::TypedTransaction, dyn_abi::TypedData, eips::eip7702::SignedAuthorization, - hex::FromHex, + hex::{self, FromHex}, + network::TxSigner, primitives::{Address, Bytes, ChainId, U256}, rpc::types::Authorization, + signers::{Signer, SignerSync}, }; use serde::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, PickFirst, serde_as}; @@ -15,7 +17,7 @@ use vault_types::enclave::encrypted::eoa::MessageFormat; use crate::{ credentials::SigningCredential, defs::AddressDef, - error::EngineError, + error::{EngineError, SerialisableAwsSdkError, SerialisableAwsSignerError}, execution_options::aa::{EntrypointAndFactoryDetails, EntrypointAndFactoryDetailsDeserHelper}, }; @@ -160,13 +162,11 @@ pub enum SigningOptions { ERC4337(Erc4337SigningOptions), } -/// Account signer trait using impl Future pattern like TWMQ pub trait AccountSigner { - type SigningOptions; /// Sign a message fn sign_message( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, message: &str, format: MessageFormat, credentials: &SigningCredential, @@ -175,7 +175,7 @@ pub trait AccountSigner { /// Sign typed data fn sign_typed_data( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, typed_data: &TypedData, credentials: &SigningCredential, ) -> impl std::future::Future> + Send; @@ -183,7 +183,7 @@ pub trait AccountSigner { /// Sign a transaction fn sign_transaction( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, transaction: &TypedTransaction, credentials: &SigningCredential, ) -> impl std::future::Future> + Send; @@ -191,7 +191,7 @@ pub trait AccountSigner { /// Sign EIP-7702 authorization fn sign_authorization( &self, - options: Self::SigningOptions, + options: EoaSigningOptions, chain_id: u64, address: Address, nonce: u64, @@ -217,8 +217,6 @@ impl EoaSigner { } impl AccountSigner for EoaSigner { - type SigningOptions = EoaSigningOptions; - async fn sign_message( &self, options: EoaSigningOptions, @@ -273,6 +271,48 @@ impl AccountSigner for EoaSigner { Ok(iaw_result.signature) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + let message = match format { + MessageFormat::Text => message.to_string().into_bytes(), + MessageFormat::Hex => { + hex::decode(message).map_err(|_| EngineError::ValidationError { + message: "Invalid hex string".to_string(), + })? + } + }; + + // TODO: create serialisable error for @alloy-signer::error::Error + let signature = signer.sign_message(&message).await.map_err(|e| { + tracing::error!("Error signing message with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + Ok(signature.to_string()) + } + SigningCredential::PrivateKey(signer) => { + let message_bytes = match format { + MessageFormat::Text => message.to_string().into_bytes(), + MessageFormat::Hex => { + alloy::hex::decode(message).map_err(|_| EngineError::ValidationError { + message: "Invalid hex string".to_string(), + })? + } + }; + + let signature = signer.sign_message(&message_bytes).await.map_err(|e| { + tracing::error!("Error signing message with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign message: {}", e), + } + })?; + Ok(signature.to_string()) + } } } @@ -310,6 +350,38 @@ impl AccountSigner for EoaSigner { Ok(iaw_result.signature) } + + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + + // TODO: create serialisable error for @alloy-signer::error::Error + let signature = signer + .sign_dynamic_typed_data(typed_data) + .await + .map_err(|e| { + tracing::error!("Error signing message with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + Ok(signature.to_string()) + } + SigningCredential::PrivateKey(signer) => { + let signature = signer + .sign_dynamic_typed_data(typed_data) + .await + .map_err(|e| { + tracing::error!("Error signing typed data with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign typed data: {}", e), + } + })?; + Ok(signature.to_string()) + } } } @@ -347,6 +419,39 @@ impl AccountSigner for EoaSigner { Ok(iaw_result.signature) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + let mut transaction = transaction.clone(); + + // TODO: create serialisable error for @alloy-signer::error::Error + let signature = signer + .sign_transaction(&mut transaction) + .await + .map_err(|e| { + tracing::error!("Error signing message with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + Ok(signature.to_string()) + } + SigningCredential::PrivateKey(signer) => { + let mut transaction = transaction.clone(); + let signature = signer + .sign_transaction(&mut transaction) + .await + .map_err(|e| { + tracing::error!("Error signing transaction with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign transaction: {}", e), + } + })?; + Ok(signature.to_string()) + } } } @@ -394,10 +499,37 @@ impl AccountSigner for EoaSigner { // Return the signed authorization as Authorization Ok(iaw_result.signed_authorization) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(options.chain_id).await?; + let authorization_hash = authorization.signature_hash(); + + let signature = signer.sign_hash(&authorization_hash).await.map_err(|e| { + tracing::error!("Error signing authorization with EOA (AWS KMS): {:?}", e); + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + + Ok(authorization.into_signed(signature)) + } + SigningCredential::PrivateKey(signer) => { + let authorization_hash = authorization.signature_hash(); + let signature = signer.sign_hash_sync(&authorization_hash).map_err(|e| { + tracing::error!("Error signing authorization with EOA (PrivateKey): {:?}", e); + EngineError::ValidationError { + message: format!("Failed to sign authorization: {}", e), + } + })?; + + Ok(authorization.into_signed(signature)) + } } } } - /// Parameters for signing a message (used in routes) pub struct MessageSignerParams { pub credentials: SigningCredential, diff --git a/core/src/userop.rs b/core/src/userop.rs index cbdefcb..e30a6ba 100644 --- a/core/src/userop.rs +++ b/core/src/userop.rs @@ -1,6 +1,7 @@ use alloy::{ hex::FromHex, primitives::{Address, Bytes, ChainId}, + signers::Signer, }; use thirdweb_core::iaw::IAWClient; use vault_sdk::VaultClient; @@ -9,7 +10,10 @@ use vault_types::{ userop::{UserOperationV06Input, UserOperationV07Input}, }; -use crate::{credentials::SigningCredential, error::EngineError}; +use crate::{ + credentials::SigningCredential, + error::{EngineError, SerialisableAwsSdkError, SerialisableAwsSignerError}, +}; // Re-export for convenience pub use engine_aa_types::VersionedUserOp; @@ -120,6 +124,41 @@ impl UserOpSigner { } })?) } + SigningCredential::AwsKms(creds) => { + let signer = creds.get_signer(Some(params.chain_id)).await?; + let userophash = params.userop.hash(params.chain_id).map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to hash userop: {}", e), + } + })?; + + let signature = signer.sign_hash(&userophash).await.map_err(|e| { + EngineError::AwsKmsSignerError { + error: SerialisableAwsSignerError::Sign { + aws_sdk_error: SerialisableAwsSdkError::Other { + message: e.to_string(), + }, + }, + } + })?; + + Ok(Bytes::copy_from_slice(&signature.as_bytes())) + } + SigningCredential::PrivateKey(signer) => { + let userophash = params.userop.hash(params.chain_id).map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to hash userop: {}", e), + } + })?; + + let signature = signer.sign_hash(&userophash).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign userop: {}", e), + } + })?; + + Ok(Bytes::copy_from_slice(&signature.as_bytes())) + } } } } diff --git a/eip7702-core/Cargo.toml b/eip7702-core/Cargo.toml index c838160..a18cf83 100644 --- a/eip7702-core/Cargo.toml +++ b/eip7702-core/Cargo.toml @@ -11,4 +11,21 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0" tracing = "0.1.41" rand = "0.9" -thiserror = "2.0" \ No newline at end of file +thiserror = "2.0" +serde_repr = "0.1.20" + +[dev-dependencies] +alloy = { workspace = true, features = [ + "sol-types", + "providers", + "transports", + "rpc-types", + "consensus", + "network", + "signers", + "node-bindings", + "serde", + "contract", + "eip712", +] } +vault-types = { workspace = true } diff --git a/eip7702-core/src/delegated_account.rs b/eip7702-core/src/delegated_account.rs index 097d9b3..8f329fa 100644 --- a/eip7702-core/src/delegated_account.rs +++ b/eip7702-core/src/delegated_account.rs @@ -6,7 +6,7 @@ use engine_core::{ chain::Chain, credentials::SigningCredential, error::{AlloyRpcErrorToEngineError, EngineError}, - signer::{AccountSigner, EoaSigner, EoaSigningOptions}, + signer::{AccountSigner, EoaSigningOptions}, }; use rand::Rng; @@ -99,9 +99,9 @@ impl DelegatedAccount { } /// Sign authorization for EIP-7702 delegation (automatically fetches nonce) - pub async fn sign_authorization( + pub async fn sign_authorization( &self, - eoa_signer: &EoaSigner, + eoa_signer: &S, credentials: &SigningCredential, ) -> Result { let nonce = self.get_nonce().await?; diff --git a/eip7702-core/src/transaction.rs b/eip7702-core/src/transaction.rs index 812fdfe..51092a1 100644 --- a/eip7702-core/src/transaction.rs +++ b/eip7702-core/src/transaction.rs @@ -12,24 +12,87 @@ use engine_core::{ transaction::InnerTransaction, }; use serde_json::Value; +use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::delegated_account::DelegatedAccount; sol!( - #[derive(serde::Serialize)] + #[derive(serde::Serialize, serde::Deserialize)] struct Call { address target; uint256 value; bytes data; } - #[derive(serde::Serialize)] + #[derive(serde::Serialize, serde::Deserialize)] struct WrappedCalls { Call[] calls; bytes32 uid; } function execute(Call[] calldata calls) external payable; + function executeWithSig(WrappedCalls calldata wrappedCalls, bytes calldata signature) external payable; + + #[derive(Serialize_repr, Deserialize_repr)] + enum LimitType { + Unlimited, + Lifetime, + Allowance, + } + + #[derive(Serialize_repr, Deserialize_repr)] + enum Condition { + Unconstrained, + Equal, + Greater, + Less, + GreaterOrEqual, + LessOrEqual, + NotEqual, + } + + #[derive(serde::Serialize)] + struct UsageLimit { + LimitType limitType; + uint256 limit; // ignored if limitType == Unlimited + uint256 period; // ignored if limitType != Allowance + } + + #[derive(serde::Serialize)] + struct Constraint { + Condition condition; + uint64 index; + bytes32 refValue; + UsageLimit limit; + } + + #[derive(serde::Serialize)] + struct CallSpec { + address target; + bytes4 selector; + uint256 maxValuePerUse; + UsageLimit valueLimit; + Constraint[] constraints; + } + + #[derive(serde::Serialize)] + struct TransferSpec { + address target; + uint256 maxValuePerUse; + UsageLimit valueLimit; + } + + #[derive(serde::Serialize)] + struct SessionSpec { + address signer; + bool isWildcard; + uint256 expiresAt; + CallSpec[] callPolicies; + TransferSpec[] transferPolicies; + bytes32 uid; + } + + function createSessionWithSig(SessionSpec calldata sessionSpec, bytes calldata signature) external; ); /// A transaction for a minimal account that supports signing and execution via bundler @@ -126,9 +189,9 @@ impl MinimalAccountTransaction { self.authorization = Some(authorization); } - pub async fn add_authorization_if_needed( + pub async fn add_authorization_if_needed( mut self, - signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, ) -> Result { if self.account.is_minimal_account().await? { @@ -141,12 +204,12 @@ impl MinimalAccountTransaction { } /// Build the transaction data as JSON for bundler execution with automatic signing - pub async fn build( + pub async fn build( &self, - eoa_signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, ) -> Result<(Value, String), EngineError> { - let signature = self.sign_wrapped_calls(eoa_signer, credentials).await?; + let signature = self.sign_wrapped_calls(signer, credentials).await?; // Serialize wrapped calls to JSON let wrapped_calls_json = serde_json::to_value(&self.wrapped_calls).map_err(|e| { @@ -190,20 +253,19 @@ impl MinimalAccountTransaction { self.authorization.as_ref() } - pub async fn sign_wrapped_calls( + pub async fn sign_wrapped_calls( &self, - eoa_signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, ) -> Result { let typed_data = self.create_wrapped_calls_typed_data(); - self.sign_typed_data(eoa_signer, credentials, &typed_data) - .await + self.sign_typed_data(signer, credentials, &typed_data).await } /// Sign typed data with EOA signer - async fn sign_typed_data( + async fn sign_typed_data( &self, - eoa_signer: &EoaSigner, + signer: &S, credentials: &SigningCredential, typed_data: &TypedData, ) -> Result { @@ -212,7 +274,7 @@ impl MinimalAccountTransaction { chain_id: Some(self.account.chain().chain_id()), }; - eoa_signer + signer .sign_typed_data(signing_options, typed_data, credentials) .await } diff --git a/eip7702-core/tests/bytecode/erc20.hex b/eip7702-core/tests/bytecode/erc20.hex new file mode 100644 index 0000000..7a86853 --- /dev/null +++ b/eip7702-core/tests/bytecode/erc20.hex @@ -0,0 +1 @@ +60806040523461031d57610bc38038038061001981610321565b92833981019060408183031261031d5780516001600160401b03811161031d5782610045918301610346565b60208201519092906001600160401b03811161031d576100659201610346565b81516001600160401b03811161023057600354600181811c91168015610313575b602082101461021257601f81116102b0575b50602092601f821160011461024f57928192935f92610244575b50508160011b915f199060031b1c1916176003555b80516001600160401b03811161023057600454600181811c91168015610226575b602082101461021257601f81116101af575b50602091601f821160011461014f579181925f92610144575b50508160011b915f199060031b1c1916176004555b601260ff19600554161760055560405161082b90816103988239f35b015190505f80610113565b601f1982169260045f52805f20915f5b8581106101975750836001951061017f575b505050811b01600455610128565b01515f1960f88460031b161c191690555f8080610171565b9192602060018192868501518155019401920161015f565b60045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f830160051c81019160208410610208575b601f0160051c01905b8181106101fd57506100fa565b5f81556001016101f0565b90915081906101e7565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100e8565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b2565b601f1982169360035f52805f20915f5b8681106102985750836001959610610280575b505050811b016003556100c7565b01515f1960f88460031b161c191690555f8080610272565b9192602060018192868501518155019401920161025f565b60035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f830160051c81019160208410610309575b601f0160051c01905b8181106102fe5750610098565b5f81556001016102f1565b90915081906102e8565b90607f1690610086565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761023057604052565b81601f8201121561031d578051906001600160401b03821161023057610375601f8301601f1916602001610321565b928284526020838301011161031d57815f9260208093018386015e830101529056fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde0314610651578063095ea7b3146105d957806318160ddd146105bc57806323b872dd14610474578063313ce5671461045457806342966c68146103d3578063449a52f81461035957806370a082311461032257806395d89b4114610204578063a0712d681461019a578063a9059cbb146100f55763dd62ed3e1461009d575f80fd5b346100f15760403660031901126100f1576100b6610750565b6001600160a01b036100c6610766565b91165f5260016020526001600160a01b0360405f2091165f52602052602060405f2054604051908152f35b5f80fd5b346100f15760403660031901126100f15761010e610750565b6001600160a01b0360243591335f525f6020526101318360405f2054101561077c565b335f525f60205260405f206101478482546107c7565b90551690815f525f60205260405f206101618282546107e8565b90556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b346100f15760203660031901126100f157600435335f525f60205260405f206101c48282546107e8565b90556101d2816002546107e8565b6002556040519081525f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203393a3005b346100f1575f3660031901126100f1576040515f6004548060011c90600181168015610318575b602083108114610304578285529081156102e85750600114610292575b50819003601f01601f191681019067ffffffffffffffff82118183101761027e576040829052819061027a9082610726565b0390f35b634e487b7160e01b5f52604160045260245ffd5b60045f9081529091507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8282106102d257506020915082010182610248565b60018160209254838588010152019101906102bd565b90506020925060ff191682840152151560051b82010182610248565b634e487b7160e01b5f52602260045260245ffd5b91607f169161022b565b346100f15760203660031901126100f1576001600160a01b03610343610750565b165f525f602052602060405f2054604051908152f35b346100f15760403660031901126100f157610372610750565b5f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206001600160a01b03602435941693848452838252604084206103b98282546107e8565b90556103c7816002546107e8565b600255604051908152a3005b346100f15760203660031901126100f1575f600435338252816020526103ff816040842054101561077c565b33825281602052604082206104158282546107c7565b9055610423816002546107c7565b6002556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3005b346100f1575f3660031901126100f157602060ff60055416604051908152f35b346100f15760603660031901126100f15761048d610750565b610495610766565b6001600160a01b03604435921690815f525f6020526104ba8360405f2054101561077c565b815f52600160205260405f206001600160a01b0333165f526020528260405f2054106105785760206001600160a01b037fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92845f525f835260405f206105218782546107c7565b90551693845f525f825260405f2061053a8282546107e8565b9055835f526001825260405f206001600160a01b0333165f52825260405f206105648282546107c7565b9055604051908152a3602060405160018152f35b606460405162461bcd60e51b815260206004820152601660248201527f496e73756666696369656e7420616c6c6f77616e6365000000000000000000006044820152fd5b346100f1575f3660031901126100f1576020600254604051908152f35b346100f15760403660031901126100f1576105f2610750565b6001600160a01b0360243591335f52600160205260405f208282165f526020528260405f205560405192835216907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b346100f1575f3660031901126100f1576040515f6003548060011c9060018116801561071c575b602083108114610304578285529081156102e857506001146106c65750819003601f01601f191681019067ffffffffffffffff82118183101761027e576040829052819061027a9082610726565b60035f9081529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b82821061070657506020915082010182610248565b60018160209254838588010152019101906106f1565b91607f1691610678565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100f157565b602435906001600160a01b03821682036100f157565b1561078357565b606460405162461bcd60e51b815260206004820152601460248201527f496e73756666696369656e742062616c616e63650000000000000000000000006044820152fd5b919082039182116107d457565b634e487b7160e01b5f52601160045260245ffd5b919082018092116107d45756fea26469706673582212200c7568949b7a79953600837c1717f912a69b1a7899a28de749c4a45445b20e6264736f6c634300081a0033 \ No newline at end of file diff --git a/eip7702-core/tests/bytecode/erc20.sol b/eip7702-core/tests/bytecode/erc20.sol new file mode 100644 index 0000000..3533f07 --- /dev/null +++ b/eip7702-core/tests/bytecode/erc20.sol @@ -0,0 +1,73 @@ +contract MockERC20 { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + uint256 public totalSupply; + string public name; + string public symbol; + uint8 public decimals; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + decimals = 18; + } + + function mint(uint256 amount) public { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + function mintTo(address to, uint256 amount) public { + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) public returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public returns (bool) { + require(balanceOf[from] >= amount, "Insufficient balance"); + require( + allowance[from][msg.sender] >= amount, + "Insufficient allowance" + ); + + balanceOf[from] -= amount; + balanceOf[to] += amount; + allowance[from][msg.sender] -= amount; + + emit Transfer(from, to, amount); + return true; + } + + function burn(uint256 amount) public { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } +} diff --git a/eip7702-core/tests/integration_tests.rs b/eip7702-core/tests/integration_tests.rs new file mode 100644 index 0000000..60f5d97 --- /dev/null +++ b/eip7702-core/tests/integration_tests.rs @@ -0,0 +1,821 @@ +use std::time::Duration; + +use alloy::{ + consensus::{SignableTransaction, TypedTransaction}, + eips::{BlockNumberOrTag, eip7702::SignedAuthorization}, + hex, + network::{EthereumWallet, TransactionBuilder, TransactionBuilder7702, TxSigner}, + node_bindings::{Anvil, AnvilInstance}, + primitives::{Address, BlockNumber, Bytes, TxHash, U256}, + providers::{ + DynProvider, Identity, Provider, ProviderBuilder, RootProvider, + ext::AnvilApi, + fillers::{ + BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, + WalletFiller, + }, + layers::AnvilProvider, + }, + rpc::types::{TransactionReceipt, TransactionRequest}, + signers::Signer, + sol, + sol_types::SolCall, +}; +use engine_core::{ + chain::Chain, + credentials::SigningCredential, + error::EngineError, + signer::{AccountSigner, EoaSigner, EoaSigningOptions}, + transaction::InnerTransaction, +}; +use engine_eip7702_core::{ + constants::MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, + delegated_account::DelegatedAccount, + transaction::{CallSpec, LimitType, SessionSpec, WrappedCalls}, +}; +use serde_json::Value; +use tokio::time::sleep; + +use crate::MockERC20::{MockERC20Calls, MockERC20Instance}; + +// Mock ERC20 contract +sol! { + #[allow(missing_docs)] + #[sol(rpc, bytecode=include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/bytecode/erc20.hex")))] + contract MockERC20 { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + uint256 public totalSupply; + string public name; + string public symbol; + uint8 public decimals; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + decimals = 18; + } + + function mint(uint256 amount) public { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + function mintTo(address to, uint256 amount) public { + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) public returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + require(balanceOf[from] >= amount, "Insufficient balance"); + require(allowance[from][msg.sender] >= amount, "Insufficient allowance"); + + balanceOf[from] -= amount; + balanceOf[to] += amount; + allowance[from][msg.sender] -= amount; + + emit Transfer(from, to, amount); + return true; + } + + function burn(uint256 amount) public { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } + } +} + +// Test chain implementation +#[derive(Clone)] +struct TestChain { + chain_id: u64, + provider: alloy::providers::DynProvider, + rpc_url: alloy::transports::http::reqwest::Url, +} + +impl Chain for TestChain { + fn chain_id(&self) -> u64 { + self.chain_id + } + + fn rpc_url(&self) -> alloy::transports::http::reqwest::Url { + self.rpc_url.clone() + } + + fn bundler_url(&self) -> alloy::transports::http::reqwest::Url { + // For tests, we'll just use the same URL + self.rpc_url.clone() + } + + fn paymaster_url(&self) -> alloy::transports::http::reqwest::Url { + // For tests, we'll just use the same URL + self.rpc_url.clone() + } + + fn provider(&self) -> &alloy::providers::RootProvider { + self.provider.root() + } + + fn bundler_client(&self) -> &engine_core::rpc_clients::BundlerClient { + // For tests, we don't need real bundler client + panic!("bundler_client not implemented for test chain") + } + + fn paymaster_client(&self) -> &engine_core::rpc_clients::PaymasterClient { + // For tests, we don't need real paymaster client + panic!("paymaster_client not implemented for test chain") + } + + fn bundler_client_with_headers( + &self, + _headers: alloy::transports::http::reqwest::header::HeaderMap, + ) -> engine_core::rpc_clients::BundlerClient { + panic!("bundler_client_with_headers not implemented for test chain") + } + + fn paymaster_client_with_headers( + &self, + _headers: alloy::transports::http::reqwest::header::HeaderMap, + ) -> engine_core::rpc_clients::PaymasterClient { + panic!("paymaster_client_with_headers not implemented for test chain") + } + + fn with_new_default_headers( + &self, + _headers: alloy::transports::http::reqwest::header::HeaderMap, + ) -> Self { + self.clone() + } +} + +// Mock EoaSigner for tests +struct MockEoaSigner; + +impl AccountSigner for MockEoaSigner { + async fn sign_message( + &self, + _options: EoaSigningOptions, + _message: &str, + _format: vault_types::enclave::encrypted::eoa::MessageFormat, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let message_bytes = _message.as_bytes(); + let signature = signer.sign_message(message_bytes).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign message: {}", e), + } + })?; + Ok(signature.to_string()) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } + + async fn sign_typed_data( + &self, + _options: EoaSigningOptions, + typed_data: &alloy::dyn_abi::TypedData, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let signature = signer + .sign_dynamic_typed_data(typed_data) + .await + .map_err(|e| EngineError::ValidationError { + message: format!("Failed to sign typed data: {}", e), + })?; + Ok(signature.to_string()) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } + + async fn sign_transaction( + &self, + _options: EoaSigningOptions, + transaction: &TypedTransaction, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let mut tx = transaction.clone(); + let signature = signer.sign_transaction(&mut tx).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign transaction: {}", e), + } + })?; + Ok(signature.to_string()) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } + + async fn sign_authorization( + &self, + _options: EoaSigningOptions, + chain_id: u64, + address: Address, + nonce: u64, + credentials: &SigningCredential, + ) -> Result { + match credentials { + SigningCredential::PrivateKey(signer) => { + let authorization = alloy::rpc::types::Authorization { + chain_id: U256::from(chain_id), + address, + nonce, + }; + let authorization_hash = authorization.signature_hash(); + let signature = signer.sign_hash(&authorization_hash).await.map_err(|e| { + EngineError::ValidationError { + message: format!("Failed to sign authorization: {}", e), + } + })?; + Ok(authorization.into_signed(signature)) + } + _ => Err(EngineError::ValidationError { + message: "Only PrivateKey credentials supported in mock".to_string(), + }), + } + } +} + +struct TestSetup { + chain: TestChain, + + anvil_provider: FillProvider< + JoinFill< + JoinFill< + Identity, + JoinFill>>, + >, + WalletFiller, + >, + AnvilProvider, + >, + mock_erc20_contract: crate::MockERC20::MockERC20Instance, + + executor_credentials: SigningCredential, + developer_credentials: SigningCredential, + user_credentials: SigningCredential, + + executor_address: Address, + developer_address: Address, + user_address: Address, + + signer: MockEoaSigner, +} + +const ANVIL_PORT: u16 = 8545; + +impl TestSetup { + async fn new() -> Result> { + // let wallet = anvil.wallet().unwrap(); + let provider = ProviderBuilder::new().connect_anvil_with_wallet_and_config(|anvil| { + anvil + .port(ANVIL_PORT) + .prague() + .chain_id(31337) + .block_time(1) + })?; + // .wallet(wallet) + // .connect_http(anvil.endpoint_url()); + + // Deploy the contract directly with sol! macro + let contract = MockERC20::deploy( + provider.clone().erased(), + "TestToken".to_string(), + "TEST".to_string(), + ) + .await?; + + println!("✅ Deployed MockERC20 at: {}", contract.address()); + let chain = TestChain { + chain_id: 31337, // Anvil default chain ID + provider: provider.clone().erased(), + rpc_url: "http://localhost:8545".parse()?, + }; + + // Create random credentials for our test EOAs + let executor_credentials = SigningCredential::random_local(); + let developer_credentials = SigningCredential::random_local(); + let user_credentials = SigningCredential::random_local(); + + // Extract addresses from the credentials + let executor_address = match &executor_credentials { + SigningCredential::PrivateKey(signer) => signer.address(), + _ => panic!("Expected PrivateKey credential"), + }; + let developer_address = match &developer_credentials { + SigningCredential::PrivateKey(signer) => signer.address(), + _ => panic!("Expected PrivateKey credential"), + }; + let user_address = match &user_credentials { + SigningCredential::PrivateKey(signer) => signer.address(), + _ => panic!("Expected PrivateKey credential"), + }; + + // Create a mock EoaSigner for tests - we'll implement the trait ourselves + let signer = MockEoaSigner; + + // Fund the test accounts + Self::fund_account(&chain, executor_address).await?; + + Ok(TestSetup { + chain, + executor_credentials, + developer_credentials, + user_credentials, + executor_address, + developer_address, + user_address, + signer, + mock_erc20_contract: contract, + anvil_provider: provider, + }) + } + + async fn fund_account( + chain: &TestChain, + address: Address, + ) -> Result<(), Box> { + // Use anvil_setBalance to fund the account + let balance = U256::from(100).pow(U256::from(18)); + let _: () = chain + .provider() + .client() + .request("anvil_setBalance", (address, format!("0x{:x}", balance))) + .await?; + + Ok(()) + } + + async fn fetch_and_set_bytecode(&self) -> Result<(), Box> { + // Fetch bytecode from Base Sepolia + let base_sepolia_url = "https://84532.rpc.thirdweb.com".parse()?; + let base_sepolia_provider = ProviderBuilder::new().connect_http(base_sepolia_url); + + let bytecode = base_sepolia_provider + .get_code_at(MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS) + .await?; + // Set bytecode on our Anvil chain + let _: () = self + .anvil_provider + .anvil_set_code(MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, bytecode) + .await?; + + println!( + "Set bytecode for minimal account implementation at {}", + MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS + ); + + Ok(()) + } + + async fn executor_broadcasts_authorization_for_account( + &self, + target_address: Address, // the account being delegated + authorization: SignedAuthorization, + ) -> Result<(), Box> { + // Executor pays gas to delegate someone else's account + let tx_request = TransactionRequest::default() + .with_from(self.executor_address) + .with_chain_id(self.chain.chain_id()) + .with_to(target_address) // Self-transaction on target to trigger delegation + .with_value(U256::ZERO) + .with_authorization_list(vec![authorization]) + .with_gas_limit(100000) + .with_max_fee_per_gas(20_000_000_000u128) // 20 gwei + .with_max_priority_fee_per_gas(1_000_000_000u128); // 1 gwei + + // Get the current nonce for executor + let nonce = self + .chain + .provider() + .get_transaction_count(self.executor_address) + .await?; + let tx_request = tx_request.with_nonce(nonce); + + let gas_fees = self.chain.provider().estimate_eip1559_fees().await?; + + let tx_request = tx_request + .with_nonce(nonce) + .with_max_fee_per_gas(gas_fees.max_fee_per_gas) + .with_max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + + // Convert to TypedTransaction for signing + let mut typed_tx: TypedTransaction = tx_request.build_typed_tx().unwrap(); + + // Executor signs the transaction + let signature_str = match &self.executor_credentials { + SigningCredential::PrivateKey(signer) => { + let sig = signer.sign_transaction(&mut typed_tx).await?; + sig.to_string() + } + _ => panic!("Expected PrivateKey credential for tests"), + }; + + // Parse signature and create signed transaction + let signature: alloy::primitives::Signature = signature_str.parse()?; + let signed_tx = typed_tx.into_signed(signature); + + // Send the transaction + let pending = self + .chain + .provider() + .send_tx_envelope(signed_tx.into()) + .await?; + let _receipt = pending.get_receipt().await?; + + Ok(()) + } + + async fn execute_wrapped_calls_via_delegated_account( + &self, + target_address: Address, + wrapped_calls: &Value, + signature: &str, + authorization: Option<&SignedAuthorization>, + ) -> Result> { + use engine_eip7702_core::transaction::executeWithSigCall; + + // Parse the wrapped calls back to the proper format + let wrapped_calls: WrappedCalls = serde_json::from_value(wrapped_calls.clone())?; + + // Create executeWithSig call data + let execute_call = executeWithSigCall { + wrappedCalls: wrapped_calls, + signature: signature.parse::()?, + }; + + let call_data = execute_call.abi_encode(); + + let tx_request = TransactionRequest::default() + .with_from(self.executor_address) + .with_chain_id(self.chain.chain_id()) + .with_to(target_address) + .with_value(U256::ZERO) + .with_input(call_data) + .with_gas_limit(500000); + + // Add authorization if provided (for initial delegation) + let tx_request = if let Some(auth) = authorization { + tx_request.with_authorization_list(vec![auth.clone()]) + } else { + tx_request + }; + + let nonce = self + .chain + .provider() + .get_transaction_count(self.executor_address) + .await?; + + let gas_fees = self.chain.provider().estimate_eip1559_fees().await?; + + let tx_request = tx_request + .with_nonce(nonce) + .with_max_fee_per_gas(gas_fees.max_fee_per_gas) + .with_max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + + let mut typed_tx: TypedTransaction = tx_request.build_typed_tx().unwrap(); + + let signature_str = match &self.executor_credentials { + SigningCredential::PrivateKey(signer) => { + let sig = signer.sign_transaction(&mut typed_tx).await?; + sig.to_string() + } + _ => panic!("Expected PrivateKey credential for tests"), + }; + + let signature: alloy::primitives::Signature = signature_str.parse()?; + let signed_tx = typed_tx.into_signed(signature); + + let pending = self + .anvil_provider + .send_tx_envelope(signed_tx.into()) + .await?; + let receipt = pending.get_receipt().await?; + + Ok(receipt) + } + + fn create_session_spec_for_granter_and_grantee(&self) -> SessionSpec { + use engine_eip7702_core::transaction::UsageLimit; + use std::time::{SystemTime, UNIX_EPOCH}; + + let expires_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + 3600; // 1 hour from now + + SessionSpec { + signer: self.developer_address, // developer (grantee) is the signer who can use this session + isWildcard: false, + expiresAt: U256::from(expires_at), + callPolicies: vec![CallSpec { + target: self.mock_erc20_contract.address().to_owned(), + selector: MockERC20::mintCall::SELECTOR.into(), + maxValuePerUse: U256::ZERO, + valueLimit: UsageLimit { + limitType: LimitType::Unlimited, + limit: U256::ZERO, + period: U256::ZERO, + }, + constraints: vec![], + }], + transferPolicies: vec![], + uid: [0u8; 32].into(), + } + } + + async fn user_signs_session_spec( + &self, + session_spec: &SessionSpec, + ) -> Result> { + use alloy::dyn_abi::TypedData; + use alloy::sol_types::eip712_domain; + + let domain = eip712_domain! { + name: "MinimalAccount", + version: "1", + chain_id: self.chain.chain_id(), + verifying_contract: self.user_address, + }; + + let typed_data = TypedData::from_struct(session_spec, Some(domain)); + + let signature = self + .signer + .sign_typed_data( + EoaSigningOptions { + from: self.user_address, + chain_id: Some(self.chain.chain_id()), + }, + &typed_data, + &self.user_credentials, + ) + .await?; + + Ok(signature) + } + + async fn executor_establishes_session_on_granter_account( + &self, + session_spec: &SessionSpec, + granter_session_signature: &str, + ) -> Result> { + use engine_eip7702_core::transaction::createSessionWithSigCall; + + let create_session_call = createSessionWithSigCall { + sessionSpec: session_spec.clone(), + signature: granter_session_signature.parse::()?, + }; + + let call_data = create_session_call.abi_encode(); + + let tx_request = TransactionRequest::default() + .with_from(self.executor_address) + .with_chain_id(self.chain.chain_id()) + .with_to(self.user_address) // call createSessionWithSig on granter's delegated account + .with_value(U256::ZERO) + .with_input(call_data) + .with_gas_limit(500000); + + let nonce = self + .chain + .provider() + .get_transaction_count(self.executor_address) + .await?; + let tx_request = tx_request.with_nonce(nonce); + + let gas_fees = self.chain.provider().estimate_eip1559_fees().await?; + + let tx_request = tx_request + .with_nonce(nonce) + .with_max_fee_per_gas(gas_fees.max_fee_per_gas) + .with_max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + + let mut typed_tx: TypedTransaction = tx_request.build_typed_tx().unwrap(); + + let signature = match &self.executor_credentials { + SigningCredential::PrivateKey(signer) => signer.sign_transaction(&mut typed_tx).await?, + _ => panic!("Expected PrivateKey credential for tests"), + }; + + let signed_tx = typed_tx.into_signed(signature); + + let pending = self + .chain + .provider() + .send_tx_envelope(signed_tx.into()) + .await?; + let receipt = pending.get_receipt().await?; + + Ok(receipt) + } +} + +#[tokio::test] +async fn test_eip7702_integration() -> Result<(), Box> { + // Set up test environment + let setup = TestSetup::new().await?; + + // Step 1: Fetch and set bytecode from Base Sepolia + setup.fetch_and_set_bytecode().await?; + // Step 2: Create delegated accounts for each EOA + let developer_account = DelegatedAccount::new(setup.developer_address, setup.chain.clone()); + let user_account = DelegatedAccount::new(setup.user_address, setup.chain.clone()); + + // Step 3: Test is_minimal_account - all should be false initially + assert!( + !developer_account.is_minimal_account().await?, + "Developer should not be minimal account initially" + ); + assert!( + !user_account.is_minimal_account().await?, + "User should not be minimal account initially" + ); + println!("✓ All accounts are not minimal accounts initially"); + + // Step 7: Test owner_transaction - developer mints tokens to themselves + let mint_call_data = MockERC20::mintCall { + amount: U256::from(1000), + } + .abi_encode(); + + let erc20_address = setup.mock_erc20_contract.address().to_owned(); + + let mint_transaction = InnerTransaction { + to: Some(erc20_address), + data: mint_call_data.into(), + value: U256::ZERO, + gas_limit: None, + transaction_type_data: None, + }; + + let developer_tx = developer_account + .clone() + .owner_transaction(&[mint_transaction]) + .add_authorization_if_needed(&setup.signer, &setup.developer_credentials) + .await?; + + let (wrapped_calls_json, signature) = developer_tx + .build(&setup.signer, &setup.developer_credentials) + .await?; + + // Execute the wrapped calls via executeWithSig on the delegated developer account + setup + .execute_wrapped_calls_via_delegated_account( + setup.developer_address, + &wrapped_calls_json, + &signature, + developer_tx.authorization(), + ) + .await?; + + // Check developer's balance - this should now reflect the executed wrapped calls + let developer_balance = setup + .mock_erc20_contract + .balanceOf(setup.developer_address) + .call() + .await?; + assert_eq!( + developer_balance, + U256::from(1000), + "Developer should have 1000 tokens" + ); + println!( + "✓ Developer successfully minted tokens using owner_transaction via EIP-7702 delegation" + ); + + assert!( + developer_account.is_minimal_account().await?, + "Developer should be minimal account after minting" + ); + + // Step 8: Delegate user account (session key granter) + // User signs authorization but executor broadcasts it (user has no funds) + let user_authorization = user_account + .sign_authorization(&setup.signer, &setup.user_credentials) + .await?; + + // Executor broadcasts the user's delegation transaction + setup + .executor_broadcasts_authorization_for_account(setup.user_address, user_authorization) + .await?; + + assert!( + user_account.is_minimal_account().await?, + "User (session key granter) should be minimal account after delegation" + ); + println!("✓ User (session key granter) is now a minimal account (delegated by executor)"); + + // Step 9: Developer is already delegated via add_authorization_if_needed in owner_transaction + assert!( + developer_account.is_minimal_account().await?, + "Developer (session key grantee) should already be minimal account from earlier delegation" + ); + println!("✓ Developer (session key grantee) was already delegated in previous step"); + + // Step 10: User (granter) creates and signs session spec allowing developer (grantee) to mint tokens + let session_spec = setup.create_session_spec_for_granter_and_grantee(); + + // User (granter) signs the session spec (EIP-712 signature) + let granter_session_signature = setup.user_signs_session_spec(&session_spec).await?; + + // Executor calls createSessionWithSig on the granter's (user's) delegated account to establish the session + let _receipt = setup + .executor_establishes_session_on_granter_account(&session_spec, &granter_session_signature) + .await?; + + println!( + "✓ Session key established: developer (grantee) can now act on behalf of user (granter)" + ); + + // Step 11: Developer (grantee) creates transaction to mint tokens for user (granter) + let mint_for_granter_transaction = InnerTransaction { + to: Some(erc20_address), + data: MockERC20::mintCall { + amount: U256::from(500), + } + .abi_encode() + .into(), + value: U256::ZERO, + gas_limit: Some(100000), + transaction_type_data: None, + }; + + // Developer (grantee) creates session_key_transaction to act on behalf of user (granter) + let grantee_session_tx = developer_account.clone().session_key_transaction( + setup.user_address, // granter's account (target) + &[mint_for_granter_transaction], + ); + + let (grantee_wrapped_calls, grantee_signature) = grantee_session_tx + .build(&setup.signer, &setup.developer_credentials) + .await?; + + // Executor executes the session: calls executeWithSig on grantee's account + // which then calls execute on granter's account + setup + .execute_wrapped_calls_via_delegated_account( + setup.developer_address, // grantee's account (where executeWithSig is called) + &grantee_wrapped_calls, + &grantee_signature, + None, // No authorization needed for grantee account (already delegated) + ) + .await?; + + sleep(Duration::from_secs(1)).await; + + // Check granter's (user's) balance increased from session key transaction + let granter_balance = setup + .mock_erc20_contract + .balanceOf(setup.user_address) + .call() + .await?; + assert_eq!( + granter_balance, + U256::from(500), + "Granter (user) should have 500 tokens from session key transaction" + ); + println!( + "✓ Session key transaction successful: grantee (developer) minted tokens for granter (user)" + ); + + println!("✓ All EIP-7702 integration tests passed!"); + + Ok(()) +} diff --git a/executors/src/eip7702_executor/send.rs b/executors/src/eip7702_executor/send.rs index 3588849..a70f5d2 100644 --- a/executors/src/eip7702_executor/send.rs +++ b/executors/src/eip7702_executor/send.rs @@ -13,7 +13,7 @@ use engine_core::{ use engine_eip7702_core::delegated_account::DelegatedAccount; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{sync::Arc, time::Duration}; +use std::{ops::Deref, sync::Arc, time::Duration}; use twmq::{ FailHookData, NackHookData, Queue, SuccessHookData, UserCancellable, error::TwmqError, @@ -227,52 +227,40 @@ where Eip7702ExecutionOptions::SessionKey(s) => Some(s.account_address), }); - let mut transactions = match session_key_target_address { + let transactions = match session_key_target_address { Some(target_address) => { account.session_key_transaction(target_address, &job_data.transactions) } None => account.owner_transaction(&job_data.transactions), }; - let is_authorization_needed = !transactions - .account() - .is_minimal_account() + let transactions = transactions + .add_authorization_if_needed(self.eoa_signer.deref(), &job_data.signing_credential) .await - .map_err(|e| Eip7702SendError::DelegationCheckFailed { inner_error: e }) - .map_err_with_max_retries( - Some(Duration::from_secs(2)), - twmq::job::RequeuePosition::Last, - 3, - job.attempts(), - )?; - - if is_authorization_needed { - let authorization = transactions - .account() - .sign_authorization(&self.eoa_signer, &job_data.signing_credential) - .await - .map_err(|e| { - let mapped_error = Eip7702SendError::SigningFailed { + .map_err(|e| { + let mapped_error = match e { + EngineError::RpcError { .. } => Eip7702SendError::DelegationCheckFailed { inner_error: e.clone(), - }; - - if is_build_error_retryable(&e) { - mapped_error.nack_with_max_retries( - Some(Duration::from_secs(2)), - twmq::job::RequeuePosition::Last, - 3, - job.attempts(), - ) - } else { - mapped_error.fail() - } - })?; + }, + _ => Eip7702SendError::SigningFailed { + inner_error: e.clone(), + }, + }; - transactions.set_authorization(authorization); - } + if is_build_error_retryable(&e) { + mapped_error.nack_with_max_retries( + Some(Duration::from_secs(2)), + twmq::job::RequeuePosition::Last, + 3, + job.attempts(), + ) + } else { + mapped_error.fail() + } + })?; let (wrapped_calls, signature) = transactions - .build(&self.eoa_signer, &job_data.signing_credential) + .build(self.eoa_signer.deref(), &job_data.signing_credential) .await .map_err(|e| Eip7702SendError::SigningFailed { inner_error: e }) .map_err_fail()?; diff --git a/executors/src/eoa/store/atomic.rs b/executors/src/eoa/store/atomic.rs index 9794e4a..e054be6 100644 --- a/executors/src/eoa/store/atomic.rs +++ b/executors/src/eoa/store/atomic.rs @@ -19,7 +19,7 @@ use crate::{ }, submitted::{ CleanAndGetRecycledNonces, CleanSubmittedTransactions, CleanupReport, - SubmittedNoopTransaction, SubmittedTransaction, + SubmittedNoopTransaction, }, }, worker::error::EoaExecutorWorkerError, diff --git a/executors/src/eoa/worker/confirm.rs b/executors/src/eoa/worker/confirm.rs index fbb9412..e21a002 100644 --- a/executors/src/eoa/worker/confirm.rs +++ b/executors/src/eoa/worker/confirm.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::eoa::{ store::{ - CleanupReport, ConfirmedTransaction, ReplacedTransaction, SubmittedTransaction, - SubmittedTransactionDehydrated, TransactionData, TransactionStoreError, + CleanupReport, ConfirmedTransaction, ReplacedTransaction, SubmittedTransactionDehydrated, + TransactionData, TransactionStoreError, }, worker::{ EoaExecutorWorker, @@ -111,7 +111,7 @@ impl EoaExecutorWorker { } // Fetch receipts and categorize transactions - let (confirmed_txs, replaced_txs) = + let (confirmed_txs, _replaced_txs) = self.fetch_confirmed_transaction_receipts(waiting_txs).await; // Process confirmed transactions diff --git a/executors/src/eoa/worker/error.rs b/executors/src/eoa/worker/error.rs index 44cb1de..cc54363 100644 --- a/executors/src/eoa/worker/error.rs +++ b/executors/src/eoa/worker/error.rs @@ -94,6 +94,17 @@ impl EoaExecutorWorkerError { inner_error: TransactionStoreError::LockLost { .. }, .. } => JobError::Fail(self), + EoaExecutorWorkerError::RpcError { .. } => { + if is_retryable_preparation_error(&self) { + JobError::Nack { + error: self, + delay: Some(Duration::from_secs(10)), + position: RequeuePosition::Last, + } + } else { + JobError::Fail(self) + } + } _ => JobError::Nack { error: self, delay: Some(Duration::from_secs(10)), @@ -219,6 +230,11 @@ pub fn is_retryable_rpc_error(kind: &RpcErrorKind) -> bool { match kind { RpcErrorKind::TransportHttpError { status, .. } if *status >= 400 && *status < 500 => false, RpcErrorKind::UnsupportedFeature { .. } => false, + RpcErrorKind::ErrorResp(resp) => { + let message = resp.message.to_lowercase(); + // if the error message contains "invalid chain", it's not retryable + !message.contains("invalid chain") + } _ => true, } } diff --git a/server/Cargo.toml b/server/Cargo.toml index 783be6d..7d89e27 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -41,3 +41,4 @@ utoipa = { version = "5.4.0", features = [ utoipa-axum = "0.2.0" utoipa-scalar = { version = "0.3.0", features = ["axum"] } serde_with = "3.14.0" +aws-arn = "0.3.1" diff --git a/server/src/http/error.rs b/server/src/http/error.rs index 827cba7..edaa18b 100644 --- a/server/src/http/error.rs +++ b/server/src/http/error.rs @@ -66,7 +66,7 @@ impl ApiEngineError { _ => StatusCode::INTERNAL_SERVER_ERROR, }, - EngineError::VaultError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + EngineError::VaultError { .. } => StatusCode::BAD_GATEWAY, EngineError::IawError { error } => match error { thirdweb_core::iaw::IAWError::ApiError { .. } => StatusCode::INTERNAL_SERVER_ERROR, thirdweb_core::iaw::IAWError::SerializationError { .. } => StatusCode::BAD_REQUEST, @@ -83,6 +83,7 @@ impl ApiEngineError { EngineError::ValidationError { .. } => StatusCode::BAD_REQUEST, EngineError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR, EngineError::ThirdwebError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + EngineError::AwsKmsSignerError { .. } => StatusCode::BAD_GATEWAY, } } } diff --git a/server/src/http/extractors.rs b/server/src/http/extractors.rs index 200cc73..928c8f7 100644 --- a/server/src/http/extractors.rs +++ b/server/src/http/extractors.rs @@ -1,15 +1,30 @@ use aide::OperationIo; +use aws_arn::known; use axum::{ Json, extract::{FromRequestParts, rejection::JsonRejection}, http::request::Parts, }; -use engine_core::{chain::RpcCredentials, credentials::SigningCredential, error::EngineError}; +use engine_core::{ + chain::RpcCredentials, + credentials::{AwsKmsCredential, SigningCredential}, + error::EngineError, +}; use thirdweb_core::auth::ThirdwebAuth; use vault_types::enclave::auth::Auth; use crate::http::error::ApiEngineError; +// Header name constants +const HEADER_THIRDWEB_SECRET_KEY: &str = "x-thirdweb-secret-key"; +const HEADER_THIRDWEB_CLIENT_ID: &str = "x-thirdweb-client-id"; +const HEADER_THIRDWEB_SERVICE_KEY: &str = "x-thirdweb-service-key"; +const HEADER_WALLET_ACCESS_TOKEN: &str = "x-wallet-access-token"; +const HEADER_VAULT_ACCESS_TOKEN: &str = "x-vault-access-token"; +const HEADER_AWS_KMS_ARN: &str = "x-aws-kms-arn"; +const HEADER_AWS_ACCESS_KEY_ID: &str = "x-aws-access-key-id"; +const HEADER_AWS_SECRET_ACCESS_KEY: &str = "x-aws-secret-access-key"; + /// Extractor for RPC credentials from headers #[derive(OperationIo)] pub struct RpcCredentialsExtractor(pub RpcCredentials); @@ -24,7 +39,7 @@ where // try secret key first let secret_key = parts .headers - .get("x-thirdweb-secret-key") + .get(HEADER_THIRDWEB_SECRET_KEY) .and_then(|v| v.to_str().ok()); if let Some(secret_key) = secret_key { @@ -36,7 +51,7 @@ where // if not, try client id and service key let client_id = parts .headers - .get("x-thirdweb-client-id") + .get(HEADER_THIRDWEB_CLIENT_ID) .and_then(|v| v.to_str().ok()) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { @@ -46,7 +61,7 @@ where let service_key = parts .headers - .get("x-thirdweb-service-key") + .get(HEADER_THIRDWEB_SERVICE_KEY) .and_then(|v| v.to_str().ok()) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { @@ -92,19 +107,107 @@ where type Rejection = ApiEngineError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - // Check for IAW credentials first (x-wallet-access-token) - // TODO: this will be deprecated in the future, we should use x-vault-access-token instead for all wallets - if let Some(wallet_token) = parts - .headers - .get("x-wallet-access-token") - .and_then(|v| v.to_str().ok()) - { - // Try client ID and service key combination - let client_id = parts - .headers - .get("x-thirdweb-client-id") - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { + // Try AWS KMS credentials first + if let Some(aws_kms) = Self::try_extract_aws_kms(parts)? { + return Ok(SigningCredentialsExtractor(SigningCredential::AwsKms( + aws_kms, + ))); + } + + // Try IAW credentials second + if let Some(iaw) = Self::try_extract_iaw(parts)? { + return Ok(SigningCredentialsExtractor(SigningCredential::Iaw { + auth_token: iaw.0, + thirdweb_auth: iaw.1, + })); + } + + // Try Vault credentials last + if let Some(vault_token) = Self::get_header_value(parts, HEADER_VAULT_ACCESS_TOKEN) { + return Ok(SigningCredentialsExtractor(SigningCredential::Vault( + Auth::AccessToken { + access_token: vault_token.to_string(), + }, + ))); + } + + // No valid credentials found + Err(ApiEngineError(EngineError::ValidationError { + message: "Missing valid authentication credentials. Provide either AWS KMS headers (x-aws-kms-arn, x-aws-kms-access-key-id, x-aws-secret-access-key), IAW credentials (x-wallet-access-token + x-thirdweb-client-id + x-thirdweb-service-key), or Vault credentials (x-vault-access-token)".to_string(), + })) + } +} + +impl SigningCredentialsExtractor { + /// Extract header value as string + fn get_header_value<'a>(parts: &'a Parts, header_name: &str) -> Option<&'a str> { + parts.headers.get(header_name).and_then(|v| v.to_str().ok()) + } + + /// Try to extract AWS KMS credentials from headers + fn try_extract_aws_kms(parts: &Parts) -> Result, ApiEngineError> { + let arn = Self::get_header_value(parts, HEADER_AWS_KMS_ARN); + let access_key_id = Self::get_header_value(parts, HEADER_AWS_ACCESS_KEY_ID); + let secret_access_key = Self::get_header_value(parts, HEADER_AWS_SECRET_ACCESS_KEY); + + match (arn, access_key_id, secret_access_key) { + (Some(arn), Some(access_key_id), Some(secret_access_key)) => { + let (key_id, region) = Self::parse_kms_arn(arn)?; + Ok(Some(AwsKmsCredential { + access_key_id: access_key_id.to_string(), + secret_access_key: secret_access_key.to_string(), + key_id, + region, + })) + } + _ => Ok(None), + } + } + + /// Parse and validate KMS ARN, returning (key_id, region) + fn parse_kms_arn(arn: &str) -> Result<(String, String), ApiEngineError> { + let parsed_arn: aws_arn::ResourceName = arn.parse().map_err(|e| { + ApiEngineError(EngineError::ValidationError { + message: format!("Invalid AWS ARN format: {}", e), + }) + })?; + + // Validate it's a KMS service + if parsed_arn.service != known::Service::KeyManagement.into() { + return Err(ApiEngineError(EngineError::ValidationError { + message: format!("ARN must be for KMS service, got: {}", parsed_arn.service), + })); + } + + // Extract and validate key ID + let key_id = parsed_arn + .resource + .path_split() + .last() + .map(|id| id.to_string()) + .ok_or_else(|| { + ApiEngineError(EngineError::ValidationError { + message: "KMS ARN must contain a valid key ID in the resource part".to_string(), + }) + })?; + + // Extract and validate region + let region = parsed_arn.region.ok_or_else(|| { + ApiEngineError(EngineError::ValidationError { + message: "KMS ARN must contain a valid region".to_string(), + }) + })?; + + Ok((key_id, region.to_string())) + } + + /// Try to extract IAW credentials from headers, returning (auth_token, thirdweb_auth) + fn try_extract_iaw(parts: &Parts) -> Result, ApiEngineError> { + let wallet_token = Self::get_header_value(parts, HEADER_WALLET_ACCESS_TOKEN); + + if let Some(wallet_token) = wallet_token { + let client_id = + Self::get_header_value(parts, HEADER_THIRDWEB_CLIENT_ID).ok_or_else(|| { ApiEngineError(EngineError::ValidationError { message: "Missing x-thirdweb-client-id header when using x-wallet-access-token" @@ -112,10 +215,7 @@ where }) })?; - let service_key = parts - .headers - .get("x-thirdweb-service-key") - .and_then(|v| v.to_str().ok()) + let service_key = Self::get_header_value(parts, HEADER_THIRDWEB_SERVICE_KEY) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { message: @@ -131,28 +231,10 @@ where }, ); - return Ok(SigningCredentialsExtractor(SigningCredential::Iaw { - auth_token: wallet_token.to_string(), - thirdweb_auth, - })); - }; - - // Fall back to Vault credentials - let vault_access_token = parts - .headers - .get("x-vault-access-token") - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { - ApiEngineError(EngineError::ValidationError { - message: "Missing x-vault-access-token or x-wallet-token header".to_string(), - }) - })?; - - Ok(SigningCredentialsExtractor(SigningCredential::Vault( - Auth::AccessToken { - access_token: vault_access_token.to_string(), - }, - ))) + Ok(Some((wallet_token.to_string(), thirdweb_auth))) + } else { + Ok(None) + } } }