diff --git a/Cargo.lock b/Cargo.lock index 1e22bfca8ca..495478e3d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,328 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-config" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848d7b9b605720989929279fa644ce8f244d0ce3146fcca5b70e4eb7b3c020fc" +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 0.2.12", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid 1.10.0", +] + +[[package]] +name = "aws-sdk-kms" +version = "1.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6550445e0913c9383375f4a5a2f550817567a19a178107fce1e1afd767f802a" +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", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a9d27ed1c12b1140c47daf1bc541606c43fdafd918c4797d520db0043ceef2" +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", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44514a6ca967686cde1e2a1b81df6ef1883d0e3e570da8d8bc5c491dcb6fc29b" +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", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7a4d279762a35b9df97209f6808b95d4fe78547fe2316b4d200a0283960c5a" +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", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8db6904450bafe7473c6ca9123f88cc11089e41a025408f992db4e22d3be68" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac 0.12.1", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "percent-encoding", + "sha2 0.10.8", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[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.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03701449087215b5369c7ea17fef0dd5d24cb93439ec5af0c7615f58c3f22605" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version 0.4.0", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -533,6 +855,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -762,6 +1094,16 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "c-kzg" version = "1.0.3" @@ -2377,6 +2719,8 @@ version = "0.63.6" dependencies = [ "anyhow", "async-trait", + "aws-config", + "aws-sdk-kms", "chrono", "clap 4.5.16", "devault", @@ -2398,6 +2742,7 @@ dependencies = [ "fuels-core", "futures", "hex", + "k256", "portpicker", "rand", "regex", @@ -4233,9 +4578,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if 1.0.0", "ecdsa", @@ -5204,6 +5549,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + [[package]] name = "overload" version = "0.1.1" @@ -6030,6 +6381,12 @@ dependencies = [ "regex-syntax 0.8.4", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -8560,6 +8917,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "vt100" version = "0.15.2" @@ -9085,6 +9448,12 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 697b359e662..0ecc97ccb14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,8 @@ anyhow = "1.0" assert-json-diff = "2.0" async-trait = "0.1" atty = "0.2" +aws-config = "1.5" +aws-sdk-kms = "1.44" byte-unit = "5.1" bytecount = "0.6" bytes = "1.7" @@ -158,6 +160,7 @@ indoc = "2.0" insta = "1.40" ipfs-api-backend-hyper = "0.6" itertools = "0.13" +k256 = "0.13" lazy_static = "1.4" libp2p-identity = "0.2" libtest-mimic = "0.7" diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index 8b7f1455daf..f63bb0b99c9 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -11,6 +11,8 @@ repository.workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true +aws-config.workspace = true +aws-sdk-kms.workspace = true chrono = { workspace = true, features = ["std"] } clap = { workspace = true, features = ["derive", "env"] } devault.workspace = true @@ -32,6 +34,7 @@ fuels-accounts.workspace = true fuels-core.workspace = true futures.workspace = true hex.workspace = true +k256.workspace = true rand.workspace = true rpassword.workspace = true serde.workspace = true diff --git a/forc-plugins/forc-client/src/cmd/deploy.rs b/forc-plugins/forc-client/src/cmd/deploy.rs index edb9e76d7f0..5553a2fe06b 100644 --- a/forc-plugins/forc-client/src/cmd/deploy.rs +++ b/forc-plugins/forc-client/src/cmd/deploy.rs @@ -87,4 +87,8 @@ pub struct Command { /// Disable the "new encoding" feature #[clap(long)] pub no_encoding_v1: bool, + + /// AWS KMS signer arn. If present forc-deploy will automatically use AWS KMS signer instead of forc-wallet. + #[clap(long)] + pub aws_kms_signer: Option, } diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 0f05e020782..e6070e0af85 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -2,12 +2,13 @@ use crate::{ cmd, constants::TX_SUBMIT_TIMEOUT_MS, util::{ + account::ForcClientAccount, node_url::get_node_url, pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, target::Target, tx::{ - bech32_from_secret, prompt_forc_wallet_password, select_secret_key, - update_proxy_contract_target, WalletSelectionMode, + prompt_forc_wallet_password, select_account, update_proxy_contract_target, + SignerSelectionMode, }, }, }; @@ -27,7 +28,7 @@ use fuels::{ programs::contract::{LoadConfiguration, StorageConfiguration}, types::{bech32::Bech32ContractId, transaction_builders::Blob}, }; -use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; +use fuels_accounts::{provider::Provider, Account, ViewOnlyAccount}; use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; use futures::FutureExt; use pkg::{manifest::build_profile::ExperimentalFlags, BuildProfile, BuiltPackage}; @@ -159,7 +160,7 @@ async fn deploy_chunked( command: &cmd::Deploy, compiled: &BuiltPackage, salt: Salt, - signing_key: &SecretKey, + account: &ForcClientAccount, provider: &Provider, pkg_name: &str, ) -> anyhow::Result { @@ -173,7 +174,6 @@ async fn deploy_chunked( None => "".to_string(), }; - let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); let blobs = compiled .bytecode .bytes @@ -184,7 +184,7 @@ async fn deploy_chunked( let tx_policies = tx_policies_from_cmd(command); let contract_id = fuels::programs::contract::Contract::loader_from_blobs(blobs, salt, storage_slots)? - .deploy(&wallet, tx_policies) + .deploy(account, tx_policies) .await? .into(); @@ -202,12 +202,11 @@ async fn deploy_new_proxy( pkg_name: &str, impl_contract: &fuel_tx::ContractId, provider: &Provider, - signing_key: &SecretKey, + account: &ForcClientAccount, ) -> Result { abigen!(Contract(name = "ProxyContract", abi = "{\"programType\":\"contract\",\"specVersion\":\"1\",\"encodingVersion\":\"1\",\"concreteTypes\":[{\"type\":\"()\",\"concreteTypeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"type\":\"enum standards::src5::AccessError\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\",\"metadataTypeId\":1},{\"type\":\"enum standards::src5::State\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"metadataTypeId\":2},{\"type\":\"enum std::option::Option\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"metadataTypeId\":4,\"typeArguments\":[\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\",\"metadataTypeId\":5},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\",\"metadataTypeId\":6},{\"type\":\"str\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"},{\"type\":\"struct std::contract_id::ContractId\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\",\"metadataTypeId\":9},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\",\"metadataTypeId\":10},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\",\"metadataTypeId\":11}],\"metadataTypes\":[{\"type\":\"b256\",\"metadataTypeId\":0},{\"type\":\"enum standards::src5::AccessError\",\"metadataTypeId\":1,\"components\":[{\"name\":\"NotOwner\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum standards::src5::State\",\"metadataTypeId\":2,\"components\":[{\"name\":\"Uninitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Initialized\",\"typeId\":3},{\"name\":\"Revoked\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum std::identity::Identity\",\"metadataTypeId\":3,\"components\":[{\"name\":\"Address\",\"typeId\":8},{\"name\":\"ContractId\",\"typeId\":9}]},{\"type\":\"enum std::option::Option\",\"metadataTypeId\":4,\"components\":[{\"name\":\"None\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Some\",\"typeId\":7}],\"typeParameters\":[7]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"metadataTypeId\":5,\"components\":[{\"name\":\"CannotReinitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"metadataTypeId\":6,\"components\":[{\"name\":\"CannotUninitialize\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"generic T\",\"metadataTypeId\":7},{\"type\":\"struct std::address::Address\",\"metadataTypeId\":8,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct std::contract_id::ContractId\",\"metadataTypeId\":9,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"metadataTypeId\":10,\"components\":[{\"name\":\"new_proxy_owner\",\"typeId\":2}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"metadataTypeId\":11,\"components\":[{\"name\":\"new_target\",\"typeId\":9}]}],\"functions\":[{\"inputs\":[],\"name\":\"proxy_target\",\"output\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [Option] - The new proxy contract to which all fallback calls will be passed or `None`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[{\"name\":\"new_target\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"}],\"name\":\"set_proxy_target\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Change the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called by the `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When not called by `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Write: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\",\"write\"]}]},{\"inputs\":[],\"name\":\"proxy_owner\",\"output\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the owner of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [State] - Represents the state of ownership for this contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[],\"name\":\"initialize_proxy\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Initializes the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This then allows methods that write to storage to be called.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called once.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When `storage::SRC14.proxy_owner` is not [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `2`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]},{\"inputs\":[{\"name\":\"new_proxy_owner\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\"}],\"name\":\"set_proxy_owner\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Changes proxy ownership to the passed State.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can be used to transfer ownership between Identities or to revoke ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_proxy_owner`: [State] - The new state of the proxy ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the sender is not the current proxy owner.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the new state of the proxy ownership is [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]}],\"loggedTypes\":[{\"logId\":\"4571204900286667806\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\"},{\"logId\":\"2151606668983994881\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\"},{\"logId\":\"2161305517876418151\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\"},{\"logId\":\"4354576968059844266\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\"},{\"logId\":\"10870989709723147660\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\"},{\"logId\":\"10098701174489624218\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"}],\"messagesTypes\":[],\"configurables\":[{\"name\":\"INITIAL_TARGET\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"offset\":13368},{\"name\":\"INITIAL_OWNER\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"offset\":13320}]}",)); let proxy_dir_output = create_proxy_contract(pkg_name)?; - let address = bech32_from_secret(signing_key)?; - let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); + let address = account.address(); let storage_path = proxy_dir_output.join("proxy-storage_slots.json"); let storage_configuration = @@ -226,7 +225,7 @@ async fn deploy_new_proxy( proxy_dir_output.join("proxy.bin"), configuration, )? - .deploy(&wallet, tx_policies) + .deploy(account, tx_policies) .await? .into(); @@ -243,7 +242,7 @@ async fn deploy_new_proxy( ); let proxy_contract_bech_id: Bech32ContractId = proxy_contract_id.into(); - let instance = ProxyContract::new(&proxy_contract_bech_id, wallet); + let instance = ProxyContract::new(&proxy_contract_bech_id, account.clone()); instance.methods().initialize_proxy().call().await?; println_action_green("Initialized", &format!("proxy contract for {pkg_name}")); Ok(proxy_contract_id) @@ -334,7 +333,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { } // Confirmation step. Summarize the transaction(s) for the deployment. - let (provider, signing_key) = + let (provider, account) = confirm_transaction_details(&pkgs_to_deploy, &command, node_url.clone()).await?; for pkg in pkgs_to_deploy { @@ -362,13 +361,13 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { &command, pkg, salt, - &signing_key, + &account, &provider, &pkg.descriptor.name, ) .await? } else { - deploy_pkg(&command, pkg, salt, &provider, &signing_key).await? + deploy_pkg(&command, pkg, salt, &provider, &account).await? }; let proxy_id = match &pkg.descriptor.manifest_file.proxy { @@ -383,13 +382,8 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { let proxy_contract = ContractId::from_str(proxy_addr).map_err(|e| anyhow::anyhow!(e))?; - update_proxy_contract_target( - &provider, - signing_key, - proxy_contract, - deployed_contract_id, - ) - .await?; + update_proxy_contract_target(&account, proxy_contract, deployed_contract_id) + .await?; Some(proxy_contract) } Some(forc_pkg::manifest::Proxy { @@ -403,7 +397,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { pkg_name, &deployed_contract_id, &provider, - &signing_key, + &account, ) .await?; @@ -433,7 +427,7 @@ async fn confirm_transaction_details( pkgs_to_deploy: &[&Arc], command: &cmd::Deploy, node_url: String, -) -> Result<(Provider, SecretKey)> { +) -> Result<(Provider, ForcClientAccount)> { // Confirmation step. Summarize the transaction(s) for the deployment. let mut tx_count = 0; let tx_summary = pkgs_to_deploy @@ -478,27 +472,28 @@ async fn confirm_transaction_details( let provider = Provider::connect(node_url.clone()).await?; let wallet_mode = if command.default_signer || command.signing_key.is_some() { - WalletSelectionMode::Manual + SignerSelectionMode::Manual + } else if let Some(arn) = &command.aws_kms_signer { + SignerSelectionMode::AwsSigner(arn.clone()) } else { println_action_green("", &format!("Wallet: {}", default_wallet_path().display())); let password = prompt_forc_wallet_password()?; - WalletSelectionMode::ForcWallet(password) + SignerSelectionMode::ForcWallet(password) }; // TODO: Display the estimated gas cost of the transaction(s). // https://github.com/FuelLabs/sway/issues/6277 - let signing_key = select_secret_key( + let account = select_account( &wallet_mode, command.default_signer || command.unsigned, command.signing_key, &provider, tx_count, ) - .await? - .ok_or_else(|| anyhow::anyhow!("failed to select a signer for the transaction"))?; + .await?; - Ok((provider.clone(), signing_key)) + Ok((provider.clone(), account)) } /// Deploy a single pkg given deploy command and the manifest file @@ -507,7 +502,7 @@ pub async fn deploy_pkg( compiled: &BuiltPackage, salt: Salt, provider: &Provider, - signing_key: &SecretKey, + account: &ForcClientAccount, ) -> Result { let manifest = &compiled.descriptor.manifest_file; let node_url = provider.url(); @@ -530,10 +525,10 @@ pub async fn deploy_pkg( storage_slots.clone(), tx_policies, ); - let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); - wallet.add_witnesses(&mut tb)?; - wallet.adjust_for_fee(&mut tb, 0).await?; + account.add_witnesses(&mut tb)?; + account.adjust_for_fee(&mut tb, 0).await?; + let tx = tb.build(provider).await?; let tx = Transaction::from(tx); diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index cda23fb14cf..5e58835fca0 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -3,10 +3,9 @@ use crate::{ cmd, constants::TX_SUBMIT_TIMEOUT_MS, util::{ - gas::get_script_gas_used, node_url::get_node_url, pkg::built_pkgs, - tx::{prompt_forc_wallet_password, TransactionBuilderExt, WalletSelectionMode}, + tx::{prompt_forc_wallet_password, select_account, SignerSelectionMode}, }, }; use anyhow::{anyhow, bail, Context, Result}; @@ -14,8 +13,16 @@ use forc_pkg::{self as pkg, fuel_core_not_running, PackageManifestFile}; use forc_tracing::println_warning; use forc_util::tx_utils::format_log_receipts; use fuel_core_client::client::FuelClient; -use fuel_tx::{ContractId, Transaction, TransactionBuilder}; -use fuels_accounts::provider::Provider; +use fuel_tx::{ContractId, Transaction}; +use fuels::{ + programs::calls::{traits::TransactionTuner, ScriptCall}, + types::{ + bech32::Bech32ContractId, + transaction::TxPolicies, + transaction_builders::{BuildableTransaction, VariableOutputPolicy}, + }, +}; +use fuels_accounts::{provider::Provider, Account}; use pkg::{manifest::build_profile::ExperimentalFlags, BuiltPackage}; use std::time::Duration; use std::{path::PathBuf, str::FromStr}; @@ -51,10 +58,10 @@ pub async fn run(command: cmd::Run) -> Result> { let build_opts = build_opts_from_cmd(&command); let built_pkgs_with_manifest = built_pkgs(&curr_dir, &build_opts)?; let wallet_mode = if command.default_signer || command.signing_key.is_some() { - WalletSelectionMode::Manual + SignerSelectionMode::Manual } else { let password = prompt_forc_wallet_password()?; - WalletSelectionMode::ForcWallet(password) + SignerSelectionMode::ForcWallet(password) }; for built in built_pkgs_with_manifest { if built @@ -77,13 +84,34 @@ pub async fn run(command: cmd::Run) -> Result> { Ok(receipts) } +fn tx_policies_from_cmd(cmd: &cmd::Run) -> TxPolicies { + let mut tx_policies = TxPolicies::default(); + if let Some(max_fee) = cmd.gas.max_fee { + tx_policies = tx_policies.with_max_fee(max_fee); + } + if let Some(script_gas_limit) = cmd.gas.script_gas_limit { + tx_policies = tx_policies.with_script_gas_limit(script_gas_limit); + } + tx_policies +} + pub async fn run_pkg( command: &cmd::Run, manifest: &PackageManifestFile, compiled: &BuiltPackage, - wallet_mode: &WalletSelectionMode, + signer_mode: &SignerSelectionMode, ) -> Result { let node_url = get_node_url(&command.node, &manifest.network)?; + let provider = Provider::connect(node_url.clone()).await?; + let tx_count = 1; + let account = select_account( + signer_mode, + command.default_signer || command.unsigned, + command.signing_key, + &provider, + tx_count, + ) + .await?; let script_data = match (&command.data, &command.args) { (None, Some(args)) => { @@ -116,31 +144,28 @@ pub async fn run_pkg( }) .collect::>>()?; - let mut tb = TransactionBuilder::script(compiled.bytecode.bytes.clone(), script_data); - tb.maturity(command.maturity.maturity.into()) - .add_contracts(contract_ids); - - let provider = Provider::connect(node_url.clone()).await?; - - let script_gas_limit = if compiled.bytecode.bytes.is_empty() { - 0 - } else if let Some(script_gas_limit) = command.gas.script_gas_limit { - script_gas_limit - // Dry run tx and get `gas_used` - } else { - get_script_gas_used(tb.clone().finalize_without_signature_inner(), &provider).await? + let script_binary = compiled.bytecode.bytes.clone(); + let external_contracts = contract_ids + .into_iter() + .map(Bech32ContractId::from) + .collect::>(); + let call = ScriptCall { + script_binary, + encoded_args: Ok(script_data), + inputs: vec![], + outputs: vec![], + external_contracts, }; - tb.script_gas_limit(script_gas_limit); - - let tx = tb - .finalize_signed( - Provider::connect(node_url.clone()).await?, - command.default_signer, - command.signing_key, - wallet_mode, - ) + let tx_policies = tx_policies_from_cmd(command); + let mut tb = call + .transaction_builder(tx_policies, VariableOutputPolicy::EstimateMinimum, &account) .await?; + account.add_witnesses(&mut tb)?; + account.adjust_for_fee(&mut tb, 0).await?; + + let tx = tb.build(provider).await?; + if command.dry_run { info!("{:?}", tx); Ok(RanScript { receipts: vec![] }) diff --git a/forc-plugins/forc-client/src/util/account.rs b/forc-plugins/forc-client/src/util/account.rs new file mode 100644 index 00000000000..17ecc6781b8 --- /dev/null +++ b/forc-plugins/forc-client/src/util/account.rs @@ -0,0 +1,87 @@ +use async_trait::async_trait; +use fuel_crypto::{Message, Signature}; +use fuels::{ + prelude::*, + types::{coin_type_id::CoinTypeId, input::Input}, +}; +use fuels_accounts::{wallet::WalletUnlocked, Account}; + +use super::aws::AwsSigner; + +#[derive(Clone, Debug)] +/// Set of different signers available to be used with `forc-client` operations. +pub enum ForcClientAccount { + /// Local signer where the private key owned locally. This can be + /// generated through `forc-wallet` integration or manually by providing + /// a private-key. + Wallet(WalletUnlocked), + /// A KMS Signer specifically using AWS KMS service. The signing key + /// is managed by another entity for KMS signers. Messages are + /// signed by the KMS entity. Signed transactions are retrieved + /// and submitted to the node by `forc-client`. + KmsSigner(AwsSigner), +} + +#[async_trait] +impl Account for ForcClientAccount { + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + excluded_coins: Option>, + ) -> Result> { + match self { + ForcClientAccount::Wallet(wallet) => { + wallet + .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) + .await + } + ForcClientAccount::KmsSigner(account) => { + account + .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) + .await + } + } + } + + fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { + tb.add_signer(self.clone())?; + + Ok(()) + } +} + +impl ViewOnlyAccount for ForcClientAccount { + fn address(&self) -> &Bech32Address { + match self { + ForcClientAccount::Wallet(wallet) => wallet.address(), + ForcClientAccount::KmsSigner(account) => { + fuels_accounts::ViewOnlyAccount::address(account) + } + } + } + + fn try_provider(&self) -> Result<&Provider> { + match self { + ForcClientAccount::Wallet(wallet) => wallet.try_provider(), + ForcClientAccount::KmsSigner(account) => Ok(account.provider()), + } + } +} + +#[async_trait] +impl Signer for ForcClientAccount { + async fn sign(&self, message: Message) -> Result { + match self { + ForcClientAccount::Wallet(wallet) => wallet.sign(message).await, + ForcClientAccount::KmsSigner(account) => account.sign(message).await, + } + } + + fn address(&self) -> &Bech32Address { + match self { + ForcClientAccount::Wallet(wallet) => wallet.address(), + ForcClientAccount::KmsSigner(account) => fuels_core::traits::Signer::address(account), + } + } +} diff --git a/forc-plugins/forc-client/src/util/aws.rs b/forc-plugins/forc-client/src/util/aws.rs new file mode 100644 index 00000000000..90da533815c --- /dev/null +++ b/forc-plugins/forc-client/src/util/aws.rs @@ -0,0 +1,269 @@ +use async_trait::async_trait; +use aws_config::{default_provider::credentials::DefaultCredentialsChain, Region, SdkConfig}; +use aws_sdk_kms::config::Credentials; +use aws_sdk_kms::operation::get_public_key::GetPublicKeyOutput; +use aws_sdk_kms::primitives::Blob; +use aws_sdk_kms::types::{MessageType, SigningAlgorithmSpec}; +use aws_sdk_kms::{config::BehaviorVersion, Client}; +use fuel_crypto::Message; +use fuels::prelude::*; +use fuels::types::bech32::{Bech32Address, FUEL_BECH32_HRP}; +use fuels::types::coin_type_id::CoinTypeId; +use fuels::types::input::Input; +use fuels_accounts::provider::Provider; +use fuels_accounts::{Account, ViewOnlyAccount}; +use fuels_core::traits::Signer; + +/// AWS configuration for the `AwsSigner` to be created. +/// De-facto way of creating the configuration is to load it from env. +#[derive(Debug, Clone)] +pub struct AwsConfig { + sdk_config: SdkConfig, +} + +impl AwsConfig { + /// Load configuration from environment variables. + /// For more details see: https://docs.rs/aws-config/latest/aws_config/ + pub async fn from_env() -> Self { + let loader = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(DefaultCredentialsChain::builder().build().await); + + let loader = match std::env::var("E2E_TEST_AWS_ENDPOINT") { + Ok(url) => loader.endpoint_url(url), + _ => loader, + }; + + Self { + sdk_config: loader.load().await, + } + } + + pub async fn for_testing(url: String) -> Self { + let sdk_config = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(Credentials::new( + "test", + "test", + None, + None, + "Static Credentials", + )) + .endpoint_url(url) + .region(Region::new("us-east-1")) // placeholder region for test + .load() + .await; + + Self { sdk_config } + } + + pub fn url(&self) -> Option<&str> { + self.sdk_config.endpoint_url() + } + + pub fn region(&self) -> Option<&Region> { + self.sdk_config.region() + } +} + +/// A configured `AwsClient` which allows using the AWS KMS SDK. +#[derive(Clone, Debug)] +pub struct AwsClient { + client: Client, +} + +impl AwsClient { + pub fn new(config: AwsConfig) -> Self { + let config = config.sdk_config; + let client = Client::new(&config); + + Self { client } + } + + pub fn inner(&self) -> &Client { + &self.client + } +} + +/// A signer which is capable of signing `fuel_crypto::Message`s using AWS KMS. +/// This is both a `Signer` and `Account`, which means it is directly usable +/// with most of the fuels-* calls, without any additional operations on the +/// representation. +#[derive(Clone, Debug)] +pub struct AwsSigner { + kms: AwsClient, + key_id: String, + bech: Bech32Address, + public_key_bytes: Vec, + provider: Provider, +} + +async fn request_get_pubkey( + kms: &Client, + key_id: String, +) -> std::result::Result { + kms.get_public_key() + .key_id(key_id) + .send() + .await + .map_err(Into::into) +} + +/// Decode an AWS KMS Pubkey response. +fn decode_pubkey(resp: &GetPublicKeyOutput) -> std::result::Result, anyhow::Error> { + let raw = resp + .public_key + .as_ref() + .ok_or(anyhow::anyhow!("public key not found"))?; + Ok(raw.clone().into_inner()) +} + +async fn sign_with_kms( + client: &aws_sdk_kms::Client, + key_id: &str, + public_key_bytes: &[u8], + message: Message, +) -> anyhow::Result { + use k256::{ + ecdsa::{RecoveryId, VerifyingKey}, + pkcs8::DecodePublicKey, + }; + + let reply = client + .sign() + .key_id(key_id) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .message_type(MessageType::Digest) + .message(Blob::new(*message)) + .send() + .await + .inspect_err(|err| tracing::error!("Failed to sign with AWS KMS: {err:?}"))?; + let signature_der = reply + .signature + .ok_or_else(|| anyhow::anyhow!("no signature returned from AWS KMS"))? + .into_inner(); + // https://stackoverflow.com/a/71475108 + let sig = k256::ecdsa::Signature::from_der(&signature_der) + .map_err(|_| anyhow::anyhow!("invalid DER signature from AWS KMS"))?; + let sig = sig.normalize_s().unwrap_or(sig); + + // This is a hack to get the recovery id. The signature should be normalized + // before computing the recovery id, but aws kms doesn't support this, and + // instead always computes the recovery id from non-normalized signature. + // So instead the recovery id is determined by checking which variant matches + // the original public key. + + let recid1 = RecoveryId::new(false, false); + let recid2 = RecoveryId::new(true, false); + + let rec1 = VerifyingKey::recover_from_prehash(&*message, &sig, recid1); + let rec2 = VerifyingKey::recover_from_prehash(&*message, &sig, recid2); + + let correct_public_key = k256::PublicKey::from_public_key_der(public_key_bytes) + .map_err(|_| anyhow::anyhow!("invalid DER public key from AWS KMS"))? + .into(); + + let recovery_id = if rec1.map(|r| r == correct_public_key).unwrap_or(false) { + recid1 + } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { + recid2 + } else { + anyhow::bail!("Invalid signature generated (reduced-x form coordinate)"); + }; + + // Insert the recovery id into the signature + debug_assert!( + !recovery_id.is_x_reduced(), + "reduced-x form coordinates are caught by the if-else chain above" + ); + let v = recovery_id.is_y_odd() as u8; + let mut signature = <[u8; 64]>::from(sig.to_bytes()); + signature[32] = (v << 7) | (signature[32] & 0x7f); + Ok(fuel_crypto::Signature::from_bytes(signature)) +} + +impl AwsSigner { + pub async fn new( + kms: AwsClient, + key_id: String, + provider: Provider, + ) -> std::result::Result { + use k256::pkcs8::DecodePublicKey; + + let resp = request_get_pubkey(kms.inner(), key_id.clone()).await?; + let public_key_bytes = decode_pubkey(&resp)?; + let k256_public_key = k256::PublicKey::from_public_key_der(&public_key_bytes)?; + + let public_key = fuel_crypto::PublicKey::from(k256_public_key); + let hashed = public_key.hash(); + let bech = Bech32Address::new(FUEL_BECH32_HRP, hashed); + Ok(Self { + kms, + key_id, + bech, + public_key_bytes, + provider, + }) + } + + /// Sign a digest with the key associated with a key ID. + pub async fn sign_message_with_key( + &self, + key_id: String, + message: Message, + ) -> std::result::Result { + sign_with_kms(self.kms.inner(), &key_id, &self.public_key_bytes, message).await + } + + /// Sign a digest with this signer's key. + pub async fn sign_message( + &self, + message: Message, + ) -> std::result::Result { + self.sign_message_with_key(self.key_id.clone(), message) + .await + } + + pub fn provider(&self) -> &Provider { + &self.provider + } +} + +#[async_trait] +impl Signer for AwsSigner { + async fn sign(&self, message: Message) -> Result { + let sig = self.sign_message(message).await.map_err(|_| { + fuels_core::types::errors::Error::Other("aws signer failed".to_string()) + })?; + Ok(sig) + } + + fn address(&self) -> &Bech32Address { + &self.bech + } +} + +impl ViewOnlyAccount for AwsSigner { + fn address(&self) -> &Bech32Address { + &self.bech + } + + fn try_provider(&self) -> Result<&Provider> { + Ok(&self.provider) + } +} + +#[async_trait] +impl Account for AwsSigner { + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + excluded_coins: Option>, + ) -> Result> { + Ok(self + .get_spendable_resources(asset_id, amount, excluded_coins) + .await? + .into_iter() + .map(Input::resource_signed) + .collect::>()) + } +} diff --git a/forc-plugins/forc-client/src/util/gas.rs b/forc-plugins/forc-client/src/util/gas.rs deleted file mode 100644 index 9667bee8674..00000000000 --- a/forc-plugins/forc-client/src/util/gas.rs +++ /dev/null @@ -1,53 +0,0 @@ -use anyhow::Result; - -use fuel_tx::{ - field::{Inputs, Witnesses}, - Buildable, Chargeable, Input, Script, TxPointer, -}; -use fuels_accounts::provider::Provider; -use fuels_core::types::transaction::ScriptTransaction; - -fn no_spendable_input<'a, I: IntoIterator>(inputs: I) -> bool { - !inputs.into_iter().any(|i| { - matches!( - i, - Input::CoinSigned(_) - | Input::CoinPredicate(_) - | Input::MessageCoinSigned(_) - | Input::MessageCoinPredicate(_) - ) - }) -} - -pub(crate) async fn get_script_gas_used(mut tx: Script, provider: &Provider) -> Result { - let no_spendable_input = no_spendable_input(tx.inputs()); - let base_asset_id = provider.base_asset_id(); - if no_spendable_input { - tx.inputs_mut().push(Input::coin_signed( - Default::default(), - Default::default(), - 1_000_000_000, - *base_asset_id, - TxPointer::default(), - 0, - )); - - // Add an empty `Witness` for the `coin_signed` we just added - // and increase the witness limit - tx.witnesses_mut().push(Default::default()); - } - let consensus_params = provider.consensus_parameters(); - - // Get `max_gas` used by everything except the script execution. Add `1` because of rounding. - let max_gas_per_tx = consensus_params.tx_params().max_gas_per_tx(); - let max_gas = tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1; - // Increase `script_gas_limit` to the maximum allowed value. - tx.set_script_gas_limit(max_gas_per_tx - max_gas); - let script_tx = ScriptTransaction::from(tx); - - let tolerance = 0.1; - let estimated_tx_cost = provider - .estimate_transaction_cost(script_tx, Some(tolerance), None) - .await?; - Ok(estimated_tx_cost.gas_used) -} diff --git a/forc-plugins/forc-client/src/util/mod.rs b/forc-plugins/forc-client/src/util/mod.rs index d8024081b28..2e58fc41c79 100644 --- a/forc-plugins/forc-client/src/util/mod.rs +++ b/forc-plugins/forc-client/src/util/mod.rs @@ -1,5 +1,6 @@ +pub mod account; +pub mod aws; pub(crate) mod encode; -pub(crate) mod gas; pub(crate) mod node_url; pub(crate) mod pkg; pub(crate) mod target; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index f826d86bf62..d5a002e809f 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -1,6 +1,8 @@ -use crate::{constants::DEFAULT_PRIVATE_KEY, util::target::Target}; -use anyhow::{Error, Result}; -use async_trait::async_trait; +use crate::{ + constants::DEFAULT_PRIVATE_KEY, + util::{account::ForcClientAccount, aws::AwsSigner, target::Target}, +}; +use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Confirm, Password, Select}; use forc_tracing::{println_action_green, println_warning}; use forc_wallet::{ @@ -11,47 +13,27 @@ use forc_wallet::{ new::{new_wallet_cli, New}, utils::default_wallet_path, }; -use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; -use fuel_tx::{ - field, Address, AssetId, Buildable, ContractId, Input, Output, TransactionBuilder, Witness, -}; +use fuel_crypto::SecretKey; +use fuel_tx::{AssetId, ContractId}; use fuels::{macros::abigen, programs::responses::CallResponse}; use fuels_accounts::{ provider::Provider, wallet::{Wallet, WalletUnlocked}, ViewOnlyAccount, }; -use fuels_core::types::{ - bech32::{Bech32Address, FUEL_BECH32_HRP}, - coin_type::CoinType, - transaction_builders::{create_coin_input, create_coin_message_input}, -}; -use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; +use std::{collections::BTreeMap, path::Path, str::FromStr}; + +use super::aws::{AwsClient, AwsConfig}; #[derive(PartialEq, Eq)] -pub enum WalletSelectionMode { +pub enum SignerSelectionMode { /// Holds the password of forc-wallet instance. ForcWallet(String), + /// Holds ARN of the AWS signer. + AwsSigner(String), Manual, } -fn prompt_address() -> Result { - print!("Please provide the address of the wallet you are going to sign this transaction with:"); - std::io::stdout().flush()?; - let mut buf = String::new(); - std::io::stdin().read_line(&mut buf)?; - Bech32Address::from_str(buf.trim()).map_err(Error::msg) -} - -fn prompt_signature(tx_id: fuel_tx::Bytes32) -> Result { - println!("Transaction id to sign: {tx_id}"); - print!("Please provide the signature:"); - std::io::stdout().flush()?; - let mut buf = String::new(); - std::io::stdin().read_line(&mut buf)?; - Signature::from_str(buf.trim()).map_err(Error::msg) -} - fn ask_user_yes_no_question(question: &str) -> Result { let answer = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(question) @@ -121,13 +103,6 @@ pub(crate) fn secret_key_from_forc_wallet( Ok(secret_key) } -pub(crate) fn bech32_from_secret(secret_key: &SecretKey) -> Result { - let public_key = PublicKey::from(secret_key); - let hashed = public_key.hash(); - let bech32 = Bech32Address::new(FUEL_BECH32_HRP, hashed); - Ok(bech32) -} - pub(crate) fn select_manual_secret_key( default_signer: bool, signing_key: Option, @@ -180,16 +155,16 @@ pub fn format_base_asset_account_balances( } // TODO: Simplify the function signature once https://github.com/FuelLabs/sway/issues/6071 is closed. -pub(crate) async fn select_secret_key( - wallet_mode: &WalletSelectionMode, +pub(crate) async fn select_account( + wallet_mode: &SignerSelectionMode, default_sign: bool, signing_key: Option, provider: &Provider, tx_count: usize, -) -> Result> { +) -> Result { let chain_info = provider.chain_info().await?; - let signing_key = match wallet_mode { - WalletSelectionMode::ForcWallet(password) => { + match wallet_mode { + SignerSelectionMode::ForcWallet(password) => { let wallet_path = default_wallet_path(); check_and_create_wallet_at_default_path(&wallet_path)?; // TODO: This is a very simple TUI, we should consider adding a nice TUI @@ -249,24 +224,34 @@ pub(crate) async fn select_secret_key( anyhow::bail!("User refused to sign"); } - Some(secret_key) + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + Ok(ForcClientAccount::Wallet(wallet)) + } + SignerSelectionMode::Manual => { + let secret_key = select_manual_secret_key(default_sign, signing_key) + .ok_or_else(|| anyhow::anyhow!("missing manual secret key"))?; + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + Ok(ForcClientAccount::Wallet(wallet)) } - WalletSelectionMode::Manual => select_manual_secret_key(default_sign, signing_key), - }; - Ok(signing_key) + SignerSelectionMode::AwsSigner(arn) => { + let aws_config = AwsConfig::from_env().await; + let aws_client = AwsClient::new(aws_config); + let aws_signer = AwsSigner::new(aws_client, arn.clone(), provider.clone()).await?; + + let account = ForcClientAccount::KmsSigner(aws_signer); + Ok(account) + } + } } pub async fn update_proxy_contract_target( - provider: &Provider, - secret_key: SecretKey, + account: &ForcClientAccount, proxy_contract_id: ContractId, new_target: ContractId, ) -> Result> { abigen!(Contract(name = "ProxyContract", abi = "{\"programType\":\"contract\",\"specVersion\":\"1\",\"encodingVersion\":\"1\",\"concreteTypes\":[{\"type\":\"()\",\"concreteTypeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"type\":\"enum standards::src5::AccessError\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\",\"metadataTypeId\":1},{\"type\":\"enum standards::src5::State\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"metadataTypeId\":2},{\"type\":\"enum std::option::Option\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"metadataTypeId\":4,\"typeArguments\":[\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\",\"metadataTypeId\":5},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\",\"metadataTypeId\":6},{\"type\":\"str\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"},{\"type\":\"struct std::contract_id::ContractId\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\",\"metadataTypeId\":9},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\",\"metadataTypeId\":10},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\",\"metadataTypeId\":11}],\"metadataTypes\":[{\"type\":\"b256\",\"metadataTypeId\":0},{\"type\":\"enum standards::src5::AccessError\",\"metadataTypeId\":1,\"components\":[{\"name\":\"NotOwner\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum standards::src5::State\",\"metadataTypeId\":2,\"components\":[{\"name\":\"Uninitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Initialized\",\"typeId\":3},{\"name\":\"Revoked\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum std::identity::Identity\",\"metadataTypeId\":3,\"components\":[{\"name\":\"Address\",\"typeId\":8},{\"name\":\"ContractId\",\"typeId\":9}]},{\"type\":\"enum std::option::Option\",\"metadataTypeId\":4,\"components\":[{\"name\":\"None\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Some\",\"typeId\":7}],\"typeParameters\":[7]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"metadataTypeId\":5,\"components\":[{\"name\":\"CannotReinitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"metadataTypeId\":6,\"components\":[{\"name\":\"CannotUninitialize\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"generic T\",\"metadataTypeId\":7},{\"type\":\"struct std::address::Address\",\"metadataTypeId\":8,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct std::contract_id::ContractId\",\"metadataTypeId\":9,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"metadataTypeId\":10,\"components\":[{\"name\":\"new_proxy_owner\",\"typeId\":2}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"metadataTypeId\":11,\"components\":[{\"name\":\"new_target\",\"typeId\":9}]}],\"functions\":[{\"inputs\":[],\"name\":\"proxy_target\",\"output\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [Option] - The new proxy contract to which all fallback calls will be passed or `None`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[{\"name\":\"new_target\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"}],\"name\":\"set_proxy_target\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Change the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called by the `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When not called by `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Write: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\",\"write\"]}]},{\"inputs\":[],\"name\":\"proxy_owner\",\"output\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the owner of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [State] - Represents the state of ownership for this contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[],\"name\":\"initialize_proxy\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Initializes the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This then allows methods that write to storage to be called.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called once.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When `storage::SRC14.proxy_owner` is not [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `2`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]},{\"inputs\":[{\"name\":\"new_proxy_owner\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\"}],\"name\":\"set_proxy_owner\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Changes proxy ownership to the passed State.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can be used to transfer ownership between Identities or to revoke ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_proxy_owner`: [State] - The new state of the proxy ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the sender is not the current proxy owner.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the new state of the proxy ownership is [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]}],\"loggedTypes\":[{\"logId\":\"4571204900286667806\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\"},{\"logId\":\"2151606668983994881\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\"},{\"logId\":\"2161305517876418151\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\"},{\"logId\":\"4354576968059844266\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\"},{\"logId\":\"10870989709723147660\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\"},{\"logId\":\"10098701174489624218\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"}],\"messagesTypes\":[],\"configurables\":[{\"name\":\"INITIAL_TARGET\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"offset\":13368},{\"name\":\"INITIAL_OWNER\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"offset\":13320}]}",)); - let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); - - let proxy_contract = ProxyContract::new(proxy_contract_id, wallet); + let proxy_contract = ProxyContract::new(proxy_contract_id, account.clone()); let result = proxy_contract .methods() @@ -280,158 +265,11 @@ pub async fn update_proxy_contract_target( Ok(result) } -#[async_trait] -pub trait TransactionBuilderExt { - fn add_contract(&mut self, contract_id: ContractId) -> &mut Self; - fn add_contracts(&mut self, contract_ids: Vec) -> &mut Self; - fn add_inputs(&mut self, inputs: Vec) -> &mut Self; - async fn fund( - &mut self, - address: Address, - provider: Provider, - signature_witness_index: u16, - ) -> Result<&mut Self>; - async fn finalize_signed( - &mut self, - client: Provider, - default_signature: bool, - signing_key: Option, - wallet_mode: &WalletSelectionMode, - ) -> Result; -} - -#[async_trait] -impl TransactionBuilderExt for TransactionBuilder { - fn add_contract(&mut self, contract_id: ContractId) -> &mut Self { - let input_index = self - .inputs() - .len() - .try_into() - .expect("limit of 256 inputs exceeded"); - self.add_input(fuel_tx::Input::contract( - fuel_tx::UtxoId::new(fuel_tx::Bytes32::zeroed(), 0), - fuel_tx::Bytes32::zeroed(), - fuel_tx::Bytes32::zeroed(), - fuel_tx::TxPointer::new(0u32.into(), 0), - contract_id, - )) - .add_output(fuel_tx::Output::Contract( - fuel_tx::output::contract::Contract { - input_index, - balance_root: fuel_tx::Bytes32::zeroed(), - state_root: fuel_tx::Bytes32::zeroed(), - }, - )) - } - fn add_contracts(&mut self, contract_ids: Vec) -> &mut Self { - for contract_id in contract_ids { - self.add_contract(contract_id); - } - self - } - fn add_inputs(&mut self, inputs: Vec) -> &mut Self { - for input in inputs { - self.add_input(input); - } - self - } - async fn fund( - &mut self, - address: Address, - provider: Provider, - signature_witness_index: u16, - ) -> Result<&mut Self> { - let asset_id = *provider.base_asset_id(); - let wallet = Wallet::from_address(Bech32Address::from(address), Some(provider)); - - let amount = 1_000_000; - let filter = None; - let inputs: Vec<_> = wallet - .get_spendable_resources(asset_id, amount, filter) - .await? - .into_iter() - .map(|coin_type| match coin_type { - CoinType::Coin(coin) => create_coin_input(coin, signature_witness_index), - CoinType::Message(message) => { - create_coin_message_input(message, signature_witness_index) - } - }) - .collect(); - let output = Output::change(wallet.address().into(), 0, asset_id); - - self.add_inputs(inputs).add_output(output); - - Ok(self) - } - async fn finalize_signed( - &mut self, - provider: Provider, - default_sign: bool, - signing_key: Option, - wallet_mode: &WalletSelectionMode, - ) -> Result { - let chain_info = provider.chain_info().await?; - let params = chain_info.consensus_parameters; - let signing_key = - select_secret_key(wallet_mode, default_sign, signing_key, &provider, 1).await?; - // Get the address - let address = if let Some(key) = signing_key { - Address::from(*key.public_key().hash()) - } else { - // TODO: Remove this path https://github.com/FuelLabs/sway/issues/6071 - Address::from(prompt_address()?) - }; - - // Insert dummy witness for signature - let signature_witness_index = self.witnesses().len().try_into()?; - self.add_witness(Witness::default()); - - // Add input coin and output change - self.fund( - address, - provider, - signature_witness_index, - ) - .await.map_err(|e| if e.to_string().contains("not enough coins to fit the target") { - anyhow::anyhow!("Deployment failed due to insufficient funds. Please be sure to have enough coins to pay for deployment transaction.") - } else { - e - })?; - - let mut tx = self.finalize_without_signature_inner(); - - let signature = if let Some(signing_key) = signing_key { - let message = Message::from_bytes(*tx.id(¶ms.chain_id())); - Signature::sign(&signing_key, &message) - } else { - prompt_signature(tx.id(¶ms.chain_id()))? - }; - - let witness = Witness::from(signature.as_ref()); - tx.replace_witness(signature_witness_index, witness); - tx.precompute(¶ms.chain_id()) - .map_err(anyhow::Error::msg)?; - - Ok(tx) - } -} - -pub trait TransactionExt { - fn replace_witness(&mut self, witness_index: u16, witness: Witness) -> &mut Self; -} - -impl TransactionExt for T { - fn replace_witness(&mut self, index: u16, witness: Witness) -> &mut Self { - self.witnesses_mut()[index as usize] = witness; - self - } -} - #[cfg(test)] mod tests { use super::*; - use std::collections::BTreeMap; - use std::collections::HashMap; + use fuels::types::bech32::Bech32Address; + use std::collections::{BTreeMap, HashMap}; #[test] fn test_format_base_asset_account_balances() { diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 0a31fb91ccb..9457bd6815c 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -2,7 +2,7 @@ use forc::cli::shared::Pkg; use forc_client::{ cmd, op::{deploy, DeployedContract}, - util::tx::update_proxy_contract_target, + util::{account::ForcClientAccount, tx::update_proxy_contract_target}, NodeTarget, }; use forc_pkg::manifest::Proxy; @@ -597,16 +597,13 @@ async fn test_non_owner_fails_to_set_target() { let dummy_contract_id_target = ContractId::default(); abigen!(Contract(name = "ProxyContract", abi = "{\"programType\":\"contract\",\"specVersion\":\"1\",\"encodingVersion\":\"1\",\"concreteTypes\":[{\"type\":\"()\",\"concreteTypeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"type\":\"enum standards::src5::AccessError\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\",\"metadataTypeId\":1},{\"type\":\"enum standards::src5::State\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"metadataTypeId\":2},{\"type\":\"enum std::option::Option\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"metadataTypeId\":4,\"typeArguments\":[\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\",\"metadataTypeId\":5},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\",\"metadataTypeId\":6},{\"type\":\"str\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"},{\"type\":\"struct std::contract_id::ContractId\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\",\"metadataTypeId\":9},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\",\"metadataTypeId\":10},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\",\"metadataTypeId\":11}],\"metadataTypes\":[{\"type\":\"b256\",\"metadataTypeId\":0},{\"type\":\"enum standards::src5::AccessError\",\"metadataTypeId\":1,\"components\":[{\"name\":\"NotOwner\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum standards::src5::State\",\"metadataTypeId\":2,\"components\":[{\"name\":\"Uninitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Initialized\",\"typeId\":3},{\"name\":\"Revoked\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum std::identity::Identity\",\"metadataTypeId\":3,\"components\":[{\"name\":\"Address\",\"typeId\":8},{\"name\":\"ContractId\",\"typeId\":9}]},{\"type\":\"enum std::option::Option\",\"metadataTypeId\":4,\"components\":[{\"name\":\"None\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"},{\"name\":\"Some\",\"typeId\":7}],\"typeParameters\":[7]},{\"type\":\"enum sway_libs::ownership::errors::InitializationError\",\"metadataTypeId\":5,\"components\":[{\"name\":\"CannotReinitialized\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"enum sway_libs::upgradability::errors::SetProxyOwnerError\",\"metadataTypeId\":6,\"components\":[{\"name\":\"CannotUninitialize\",\"typeId\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\"}]},{\"type\":\"generic T\",\"metadataTypeId\":7},{\"type\":\"struct std::address::Address\",\"metadataTypeId\":8,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct std::contract_id::ContractId\",\"metadataTypeId\":9,\"components\":[{\"name\":\"bits\",\"typeId\":0}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyOwnerSet\",\"metadataTypeId\":10,\"components\":[{\"name\":\"new_proxy_owner\",\"typeId\":2}]},{\"type\":\"struct sway_libs::upgradability::events::ProxyTargetSet\",\"metadataTypeId\":11,\"components\":[{\"name\":\"new_target\",\"typeId\":9}]}],\"functions\":[{\"inputs\":[],\"name\":\"proxy_target\",\"output\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [Option] - The new proxy contract to which all fallback calls will be passed or `None`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[{\"name\":\"new_target\",\"concreteTypeId\":\"29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54\"}],\"name\":\"set_proxy_target\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Change the target contract of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called by the `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When not called by `proxy_owner`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Write: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\",\"write\"]}]},{\"inputs\":[],\"name\":\"proxy_owner\",\"output\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Returns the owner of the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Returns\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * [State] - Represents the state of ownership for this contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"read\"]}]},{\"inputs\":[],\"name\":\"initialize_proxy\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Initializes the proxy contract.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This then allows methods that write to storage to be called.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can only be called once.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When `storage::SRC14.proxy_owner` is not [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `2`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]},{\"inputs\":[{\"name\":\"new_proxy_owner\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\"}],\"name\":\"set_proxy_owner\",\"output\":\"2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d\",\"attributes\":[{\"name\":\"doc-comment\",\"arguments\":[\" Changes proxy ownership to the passed State.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Additional Information\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" This method can be used to transfer ownership between Identities or to revoke ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Arguments\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * `new_proxy_owner`: [State] - The new state of the proxy ownership.\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Reverts\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the sender is not the current proxy owner.\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * When the new state of the proxy ownership is [State::Uninitialized].\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" # Number of Storage Accesses\"]},{\"name\":\"doc-comment\",\"arguments\":[\"\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Reads: `1`\"]},{\"name\":\"doc-comment\",\"arguments\":[\" * Writes: `1`\"]},{\"name\":\"storage\",\"arguments\":[\"write\"]}]}],\"loggedTypes\":[{\"logId\":\"4571204900286667806\",\"concreteTypeId\":\"3f702ea3351c9c1ece2b84048006c8034a24cbc2bad2e740d0412b4172951d3d\"},{\"logId\":\"2151606668983994881\",\"concreteTypeId\":\"1ddc0adda1270a016c08ffd614f29f599b4725407c8954c8b960bdf651a9a6c8\"},{\"logId\":\"2161305517876418151\",\"concreteTypeId\":\"1dfe7feadc1d9667a4351761230f948744068a090fe91b1bc6763a90ed5d3893\"},{\"logId\":\"4354576968059844266\",\"concreteTypeId\":\"3c6e90ae504df6aad8b34a93ba77dc62623e00b777eecacfa034a8ac6e890c74\"},{\"logId\":\"10870989709723147660\",\"concreteTypeId\":\"96dd838b44f99d8ccae2a7948137ab6256c48ca4abc6168abc880de07fba7247\"},{\"logId\":\"10098701174489624218\",\"concreteTypeId\":\"8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a\"}],\"messagesTypes\":[],\"configurables\":[{\"name\":\"INITIAL_TARGET\",\"concreteTypeId\":\"0d79387ad3bacdc3b7aad9da3a96f4ce60d9a1b6002df254069ad95a3931d5c8\",\"offset\":13368},{\"name\":\"INITIAL_OWNER\",\"concreteTypeId\":\"192bc7098e2fe60635a9918afb563e4e5419d386da2bdbf0d716b4bc8549802c\",\"offset\":13320}]}",)); + let wallet = WalletUnlocked::new_from_private_key(attacker_secret_key, Some(provider.clone())); + let attacker_account = ForcClientAccount::Wallet(wallet); // Try to change target of the proxy with a random wallet which is not the owner of the proxy. - let res = update_proxy_contract_target( - &provider, - attacker_secret_key, - proxy_id, - dummy_contract_id_target, - ) - .await - .err() - .unwrap(); + let res = update_proxy_contract_target(&attacker_account, proxy_id, dummy_contract_id_target) + .await + .err() + .unwrap(); node.kill().unwrap(); assert!(res