From bb328f1ac5ea54593a5f2d8a325a01843e563bcd Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Wed, 12 Feb 2025 09:20:57 +0100 Subject: [PATCH 01/34] add kms support --- Cargo.toml | 5 + e2e/Cargo.toml | 25 + e2e/src/client.rs | 61 +++ e2e/src/fuel_node.rs | 206 +++++++++ e2e/src/kms.rs | 160 +++++++ e2e/src/lib.rs | 43 ++ e2e/tests/wallets.rs | 7 + packages/fuels-accounts/Cargo.toml | 7 + packages/fuels-accounts/src/aws_signer.rs | 539 ++++++++++++++++++++++ packages/fuels-accounts/src/lib.rs | 3 + 10 files changed, 1056 insertions(+) create mode 100644 e2e/src/client.rs create mode 100644 e2e/src/fuel_node.rs create mode 100644 e2e/src/kms.rs create mode 100644 e2e/src/lib.rs create mode 100644 packages/fuels-accounts/src/aws_signer.rs diff --git a/Cargo.toml b/Cargo.toml index f78f89c4e..c29074f6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,11 @@ dotenv = { version = "0.15", default-features = false } toml = { version = "0.8", default-features = false } mockall = { version = "0.13", default-features = false } +aws-config = { version = "1.5.5", default-features = false } +aws-sdk-kms = { version = "1.36", default-features = false } +testcontainers = { version = "0.20", default-features = false } +k256 = { version = "0.13.3", default-features = false } + # Dependencies from the `fuel-core` repository: fuel-core = { version = "0.41.3", default-features = false, features = [ "wasm-executor", diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 1dc6a7110..cecc1d7f9 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -24,6 +24,28 @@ tai64 = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["test-util"] } +itertools = { workspace = true } +futures = { workspace = true } +rand = { workspace = true } +portpicker = { workspace = true } + +fuel-core-chain-config = { workspace = true, features = [ + "std", + "test-helpers", +] } + +fuel-core-types = { workspace = true, features = [ + "da-compression", +] } + +testcontainers = { workspace = true } +k256 = { workspace = true, features = ["ecdsa-core"] } +aws-config = { workspace = true, features = ["rustls"] } +aws-sdk-kms = { workspace = true, features = ["rustls"] } +fuel-core-client = { workspace = true } + + + [build-dependencies] anyhow = { workspace = true, features = ["std"] } flate2 = { workspace = true, features = ["zlib"] } @@ -37,3 +59,6 @@ default = ["fuels/default", "coin-cache"] fuel-core-lib = ["fuels/fuel-core-lib"] rocksdb = ["fuels/rocksdb"] coin-cache = ["fuels/coin-cache"] +[dependencies] +anyhow = "1.0.86" +url = "2.5.2" \ No newline at end of file diff --git a/e2e/src/client.rs b/e2e/src/client.rs new file mode 100644 index 000000000..6771eec54 --- /dev/null +++ b/e2e/src/client.rs @@ -0,0 +1,61 @@ +use url::Url; + +use fuel_core_client::client::types::CoinType; +use fuel_core_client::client::{types::Block, FuelClient}; +use fuel_core_types::fuel_tx::Transaction; +use fuel_core_types::fuel_types::{Address, AssetId}; +use fuels::types::coin::Coin; +use fuels::types::errors::Error; +use fuels::types::errors::Result; +#[derive(Clone)] +pub struct HttpClient { + client: FuelClient, +} + +impl HttpClient { + #[must_use] + pub fn new(url: &Url) -> Self { + let client = FuelClient::new(url).expect("Url to be well formed"); + Self { client } + } + + pub async fn produce_blocks(&self, num: u32) -> Result<()> { + self.client + .produce_blocks(num, None) + .await + .map_err(|e| Error::Other(e.to_string()))?; + + Ok(()) + } + + pub async fn send_tx(&self, tx: &Transaction) -> Result<()> { + self.client + .submit_and_await_commit(tx) + .await + .map_err(|e| Error::Other(e.to_string()))?; + + Ok(()) + } + + pub async fn get_coin(&self, address: Address, asset_id: AssetId) -> Result { + let coin_type = self + .client + .coins_to_spend(&address, vec![(asset_id, 1, None)], None) + .await + .map_err(|e| Error::Other(e.to_string()))?[0][0]; + + let coin = match coin_type { + CoinType::Coin(c) => Ok(c), + _ => Err(Error::Other("Couldn't get coin".to_string())), + }?; + + Ok(Coin::from(coin)) + } + + pub async fn health(&self) -> Result { + match self.client.health().await { + Ok(healthy) => Ok(healthy), + Err(err) => Err(Error::Other(err.to_string())), + } + } +} diff --git a/e2e/src/fuel_node.rs b/e2e/src/fuel_node.rs new file mode 100644 index 000000000..9653a6f9e --- /dev/null +++ b/e2e/src/fuel_node.rs @@ -0,0 +1,206 @@ +use crate::client::HttpClient; +use fuel_core_chain_config::{ + ChainConfig, CoinConfig, ConsensusConfig, SnapshotWriter, StateConfig, +}; +use fuel_core_types::{ + fuel_crypto::SecretKey as FuelSecretKey, + fuel_tx::{AssetId, Finalizable, Input, Output, TransactionBuilder, TxPointer}, + fuel_types::Address, +}; +use fuels::crypto::{PublicKey, SecretKey}; +use fuels::prelude::{Provider, WalletUnlocked}; +use itertools::Itertools; +use rand::Rng; +use std::path::PathBuf; +use std::str::FromStr; +use url::Url; +use fuels::accounts::Account; +use fuels::accounts::aws_signer::AwsWallet; +use fuels::types::U256; + +#[derive(Default, Debug)] +pub struct FuelNode { + show_logs: bool, +} + +pub struct FuelNodeProcess { + _child: tokio::process::Child, + url: Url, +} + +impl FuelNode { + fn create_state_config( + path: impl Into, + consensus_key: &PublicKey, + num_wallets: usize, + ) -> anyhow::Result> { + let chain_config = ChainConfig { + consensus: ConsensusConfig::PoA { + signing_key: Input::owner(consensus_key), + }, + ..ChainConfig::local_testnet() + }; + + let mut rng = &mut rand::thread_rng(); + let keys = std::iter::repeat_with(|| FuelSecretKey::random(&mut rng)) + .take(num_wallets) + .collect_vec(); + + let coins = keys + .iter() + .flat_map(|key| { + std::iter::repeat_with(|| CoinConfig { + owner: Input::owner(&key.public_key()), + amount: u64::MAX, + asset_id: AssetId::zeroed(), + tx_id: rng.gen(), + output_index: rng.gen(), + ..Default::default() + }) + .take(10) + .collect_vec() + }) + .collect_vec(); + + let state_config = StateConfig { + coins, + ..StateConfig::local_testnet() + }; + + let snapshot = SnapshotWriter::json(path); + snapshot + .write_state_config(state_config, &chain_config) + .map_err(|_| anyhow::anyhow!("Failed to write state config"))?; + + Ok(keys) + } + + pub async fn start(&self) -> anyhow::Result { + let unused_port = portpicker::pick_unused_port() + .ok_or_else(|| anyhow::anyhow!("No free port to start fuel-core"))?; + + let mut cmd = tokio::process::Command::new("fuel-core"); + + cmd.arg("run") + .arg("--port") + .arg(unused_port.to_string()) + .arg("--db-type") + .arg("in-memory") + .arg("--debug") + .kill_on_drop(true) + .stdin(std::process::Stdio::null()); + + let sink = if self.show_logs { + std::process::Stdio::inherit + } else { + std::process::Stdio::null + }; + cmd.stdout(sink()).stderr(sink()); + + let child = cmd.spawn()?; + + let url = format!("http://localhost:{}", unused_port).parse()?; + + let process = FuelNodeProcess { _child: child, url }; + + process.wait_until_healthy().await; + + Ok(process) + } + + pub fn with_show_logs(mut self, show_logs: bool) -> Self { + self.show_logs = show_logs; + self + } +} + +impl FuelNodeProcess { + pub fn client(&self) -> HttpClient { + HttpClient::new(&self.url) + } + + async fn send_transfer_tx(client: HttpClient, key: FuelSecretKey) -> anyhow::Result<()> { + let mut tx = TransactionBuilder::script(vec![], vec![]); + + tx.script_gas_limit(1_000_000); + + let secret_key = key; + let address = Input::owner(&secret_key.public_key()); + + let base_asset = AssetId::zeroed(); + let coin = client.get_coin(address, base_asset).await?; + + tx.add_unsigned_coin_input( + secret_key, + coin.utxo_id, + coin.amount, + coin.asset_id, + TxPointer::default(), + ); + + const AMOUNT: u64 = 1; + let to = Address::default(); + tx.add_output(Output::Coin { + to, + amount: AMOUNT, + asset_id: base_asset, + }); + tx.add_output(Output::Change { + to: address, + amount: 0, + asset_id: base_asset, + }); + + let tx = tx.finalize(); + + client.send_tx(&tx.into()).await?; + + Ok(()) + } + + async fn wait_until_healthy(&self) { + loop { + if let Ok(true) = self.client().health().await { + break; + } + } + } + + pub fn url(&self) -> &Url { + &self.url + } + + pub async fn fund( + &self, + address: Address, + amount: U256 + ) -> anyhow::Result<()> { + let fuels_provider = Provider::connect(self.url()).await.unwrap(); + + let mut default_wallet = WalletUnlocked::new_from_private_key( + SecretKey::from_str( + "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c", + ) + .unwrap(), + None, + ); + + default_wallet.set_provider(fuels_provider.clone()); + // let wallet = AwsWallet::from_kms_key_id(key.id, provider).await?; + + // default_wallet + // .transfer( + // wallet.address(), + // 1_000_000_000, // Amount to transfer + // AssetId::default(), // Using the base asset + // TxParams::default(), + // ) + // .await?; + // if succeeded { + // Ok(()) + // } else { + // Err(anyhow::anyhow!("Failed to fund address {address}")) + // } + Ok(()) + } +} diff --git a/e2e/src/kms.rs b/e2e/src/kms.rs new file mode 100644 index 000000000..49a5bb890 --- /dev/null +++ b/e2e/src/kms.rs @@ -0,0 +1,160 @@ +use anyhow::Context; +use aws_config::Region; +use aws_sdk_kms::{config::Credentials, Client as AWSClient}; +use aws_sdk_kms::config::BehaviorVersion; +use testcontainers::{core::ContainerPort, runners::AsyncRunner}; +use tokio::io::AsyncBufReadExt; +use fuels::accounts::aws_signer::KmsData; +#[derive(Default)] +pub struct Kms { + show_logs: bool, +} + +struct KmsImage; + +impl testcontainers::Image for KmsImage { + fn name(&self) -> &str { + "localstack/localstack" + } + + fn tag(&self) -> &str { + "latest" + } + + fn ready_conditions(&self) -> Vec { + vec![testcontainers::core::WaitFor::message_on_stdout("Ready.")] + } + + fn expose_ports(&self) -> &[ContainerPort] { + &[ContainerPort::Tcp(4566)] + } +} + +impl Kms { + pub fn with_show_logs(mut self, show_logs: bool) -> Self { + self.show_logs = show_logs; + self + } + + pub async fn start(self) -> anyhow::Result { + let container = KmsImage + .start() + .await + .with_context(|| "Failed to start KMS container")?; + + if self.show_logs { + spawn_log_printer(&container); + } + + let port = container.get_host_port_ipv4(4566).await?; + let url = format!("http://localhost:{}", port); + + // Configure AWS SDK + // let config = aws_config::from_env() + // .endpoint_url(url.clone()) + // .region("us-east-1") + // .credentials_provider(Credentials::new("test", "test", None, None, "test")) + // .load() + // .await; + + let config = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(Credentials::new( + "test", + "test", + None, + None, + "Static Credentials", + )) + .endpoint_url(url.clone()) + .region(Region::new("us-east-1")) // placeholder region for test + .load() + .await; + + let client = AWSClient::new(&config); + + Ok(KmsProcess { + _container: container, + client, + url, + }) + } +} + +fn spawn_log_printer(container: &testcontainers::ContainerAsync) { + let stderr = container.stderr(true); + let stdout = container.stdout(true); + tokio::spawn(async move { + let mut stderr_lines = stderr.lines(); + let mut stdout_lines = stdout.lines(); + + let mut other_stream_closed = false; + loop { + tokio::select! { + stderr_result = stderr_lines.next_line() => { + match stderr_result { + Ok(Some(line)) => eprintln!("KMS (stderr): {}", line), + Ok(None) if other_stream_closed => break, + Ok(None) => other_stream_closed = true, + Err(e) => { + eprintln!("KMS: Error reading from stderr: {:?}", e); + break; + } + } + } + stdout_result = stdout_lines.next_line() => { + match stdout_result { + Ok(Some(line)) => eprintln!("KMS (stdout): {}", line), + Ok(None) if other_stream_closed => break, + Ok(None) => other_stream_closed = true, + Err(e) => { + eprintln!("KMS: Error reading from stdout: {:?}", e); + break; + } + } + } + } + } + + Ok::<(), std::io::Error>(()) + }); +} + +pub struct KmsProcess { + _container: testcontainers::ContainerAsync, + client: AWSClient, + url: String, +} + +impl KmsProcess { + pub async fn create_key(&self) -> anyhow::Result { + let response = self + .client + .create_key() + .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) + .key_spec(aws_sdk_kms::types::KeySpec::EccSecgP256K1) + .send() + .await?; + + // use arn as id to closer imitate prod behavior + let id = response + .key_metadata + .and_then(|metadata| metadata.arn) + .ok_or_else(|| anyhow::anyhow!("key arn missing from response"))?; + + let kms_data = KmsData::new(id.clone(), self.client.clone()).await?; + + Ok(KmsKey { + id, + kms_data,// todo fix this + url: self.url.clone(), + }) + } +} + + +#[derive(Debug, Clone)] +pub struct KmsKey { + pub id: String, + pub kms_data: KmsData, + pub url: String, +} diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs new file mode 100644 index 000000000..8abe2157b --- /dev/null +++ b/e2e/src/lib.rs @@ -0,0 +1,43 @@ +#[cfg(test)] +mod client; +#[cfg(test)] +mod fuel_node; +#[cfg(test)] +mod kms; + +#[cfg(test)] +mod tests { + use crate::fuel_node::FuelNode; + use anyhow::Result; + use fuel_core_client::client::schema::schema::__fields::GasCosts::tro; + use fuels::types::U256; + use crate::kms::Kms; + + #[tokio::test(flavor = "multi_thread")] + async fn aws_wallet() -> Result<()> { + + let kms = Kms::default().with_show_logs(false).start().await?; + let key = kms.create_key().await?; + let fuel_node = FuelNode::default().with_show_logs(false).start().await?; + + fuel_node.fund(key.kms_data.address.into(), U256::from(10)).await?; + + // let g = Url::parse("http://localhost:4000").unwrap(); + + // let fuels_provider = Provider::connect(self.url()).await.unwrap(); + // let fuels_provider = Provider::connect(g).await.unwrap(); + // + // let mut wallet = WalletUnlocked::new_from_private_key( + // SecretKey::from_str("0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c").unwrap(), + // None, + // ); + // + // wallet.set_provider(fuels_provider.clone()); + // + // dbg!(fuels_provider.get_balances(wallet.address()).await?); + // dbg!(fuels_provider.url()); + + + Ok(()) + } +} diff --git a/e2e/tests/wallets.rs b/e2e/tests/wallets.rs index 43ab1d319..352756bdd 100644 --- a/e2e/tests/wallets.rs +++ b/e2e/tests/wallets.rs @@ -557,3 +557,10 @@ async fn wallet_transfer_respects_maturity_and_expiration() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn aws_kms() -> Result<()> { + FuelNode::start().await.expect("TODO: panic message"); + + Ok(()) +} diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 069b04ac8..574dd286d 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -28,6 +28,13 @@ tai64 = { workspace = true, features = ["serde"] } thiserror = { workspace = true, default-features = false } tokio = { workspace = true, features = ["full"], optional = true } zeroize = { workspace = true, features = ["derive"] } +anyhow = "1.0.86" + +k256 = { workspace = true, features = ["ecdsa-core"] } +aws-config = { workspace = true, features = [ + "behavior-version-latest", +] } +aws-sdk-kms = { workspace = true } [dev-dependencies] mockall = { workspace = true, default-features = false } diff --git a/packages/fuels-accounts/src/aws_signer.rs b/packages/fuels-accounts/src/aws_signer.rs new file mode 100644 index 000000000..6e5791215 --- /dev/null +++ b/packages/fuels-accounts/src/aws_signer.rs @@ -0,0 +1,539 @@ +use aws_sdk_kms::{ + primitives::Blob, + types::{KeySpec, MessageType, SigningAlgorithmSpec}, + Client as AwsClient, +}; +use fuel_crypto::{Message, PublicKey, Signature}; +use fuel_types::AssetId; +use fuels_core::{ + traits::Signer, + types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + coin_type_id::CoinTypeId, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + }, +}; +use k256::{ + ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}, + pkcs8::DecodePublicKey, + PublicKey as K256PublicKey, +}; + +use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; + +const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; + +#[derive(Clone, Debug)] +pub struct AwsWallet { + wallet: Wallet, + kms_data: KmsData, +} + +#[derive(Clone, Debug)] +pub struct KmsData { + id: String, + client: AwsClient, + public_key: Vec, + pub address: Bech32Address, +} + +impl KmsData { + pub async fn new(id: String, client: AwsClient) -> anyhow::Result { + Self::validate_key_type(&client, &id).await?; + let public_key = Self::fetch_public_key(&client, &id).await?; + let address = Self::create_bech32_address(&public_key)?; + + Ok(Self { + id, + client, + public_key, + address, + }) + } + + async fn validate_key_type(client: &AwsClient, key_id: &str) -> anyhow::Result<()> { + let key_spec = client + .get_public_key() + .key_id(key_id) + .send() + .await? + .key_spec; + + match key_spec { + Some(KeySpec::EccSecgP256K1) => Ok(()), + other => anyhow::bail!( + "{}: Invalid key type, expected EccSecgP256K1, got {:?}", + AWS_KMS_ERROR_PREFIX, + other + ), + } + } + + async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { + let response = client.get_public_key().key_id(key_id).send().await?; + + let public_key = response + .public_key() + .ok_or_else(|| anyhow::anyhow!("{}: No public key returned", AWS_KMS_ERROR_PREFIX))?; + + Ok(public_key.clone().into_inner()) + } + + fn create_bech32_address(public_key_bytes: &[u8]) -> anyhow::Result { + let k256_public_key = K256PublicKey::from_public_key_der(public_key_bytes) + .map_err(|_| anyhow::anyhow!("{}: Invalid DER public key", AWS_KMS_ERROR_PREFIX))?; + + let public_key = PublicKey::from(k256_public_key); + let fuel_address = public_key.hash(); + + Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_address)) + } + + async fn sign_message(&self, message: Message) -> Result { + let signature_der = self.request_signature(message).await?; + let (signature, recovery_id) = self.process_signature(&signature_der, message)?; + + Ok(self.create_fuel_signature(signature, recovery_id)) + } + + async fn request_signature(&self, message: Message) -> Result> { + let reply = self + .client + .sign() + .key_id(&self.id) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .message_type(MessageType::Digest) + .message(Blob::new(*message)) + .send() + .await + .map_err(|e| { + Error::Other(format!("{}: Failed to sign: {:?}", AWS_KMS_ERROR_PREFIX, e)) + })?; + + reply + .signature + .map(|sig| sig.into_inner()) + .ok_or_else(|| Error::Other(format!("{}: No signature returned", AWS_KMS_ERROR_PREFIX))) + } + + fn process_signature( + &self, + signature_der: &[u8], + message: Message, + ) -> Result<(K256Signature, RecoveryId)> { + let sig = K256Signature::from_der(signature_der).map_err(|_| { + Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) + })?; + let sig = sig.normalize_s().unwrap_or(sig); + + let recovery_id = self.determine_recovery_id(&sig, message)?; + + Ok((sig, recovery_id)) + } + + fn determine_recovery_id(&self, sig: &K256Signature, message: Message) -> Result { + let recid1 = RecoveryId::new(false, false); + let recid2 = RecoveryId::new(true, false); + + let correct_public_key = K256PublicKey::from_public_key_der(&self.public_key) + .map_err(|_| { + Error::Other(format!( + "{}: Invalid cached public key", + AWS_KMS_ERROR_PREFIX + )) + })? + .into(); + + let rec1 = VerifyingKey::recover_from_prehash(&*message, sig, recid1); + let rec2 = VerifyingKey::recover_from_prehash(&*message, sig, recid2); + + if rec1.map(|r| r == correct_public_key).unwrap_or(false) { + Ok(recid1) + } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { + Ok(recid2) + } else { + Err(Error::Other(format!( + "{}: Invalid signature (reduced-x form coordinate)", + AWS_KMS_ERROR_PREFIX + ))) + } + } + + fn create_fuel_signature( + &self, + signature: K256Signature, + recovery_id: RecoveryId, + ) -> Signature { + debug_assert!( + !recovery_id.is_x_reduced(), + "reduced-x form coordinates should be caught earlier" + ); + + let v = recovery_id.is_y_odd() as u8; + let mut signature_bytes = <[u8; 64]>::from(signature.to_bytes()); + signature_bytes[32] = (v << 7) | (signature_bytes[32] & 0x7f); + + Signature::from_bytes(signature_bytes) + } +} + +impl AwsWallet { + pub async fn from_kms_key_id( + kms_key_id: String, + provider: Option, + ) -> anyhow::Result { + let config = aws_config::load_from_env().await; + let client = AwsClient::new(&config); + + let kms_data = KmsData::new(kms_key_id, client).await?; + println!("Fuel address: {}", kms_data.address); + + Ok(Self { + wallet: Wallet::from_address(kms_data.address.clone(), provider), + kms_data, + }) + } + + pub fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + pub fn provider(&self) -> Option<&Provider> { + self.wallet.provider() + } +} + +#[async_trait::async_trait] +impl Signer for AwsWallet { + async fn sign(&self, message: Message) -> Result { + self.kms_data.sign_message(message).await + } + + fn address(&self) -> &Bech32Address { + &self.kms_data.address + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl ViewOnlyAccount for AwsWallet { + fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + fn try_provider(&self) -> Result<&Provider> { + self.wallet.provider().ok_or_else(|| { + Error::Other("No provider available. Make sure to use `set_provider`".to_owned()) + }) + } + + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + excluded_coins: Option>, + ) -> Result> { + self.wallet + .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) + .await + } +} + +#[async_trait::async_trait] +impl Account for AwsWallet { + fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { + tb.add_signer(self.clone())?; + Ok(()) + } +} + +// +// // Integration tests using the LocalStack KMS +// // #[cfg(test)] +// // mod tests { +// // use super::*; +// // use fuel_crypto::Message; +// // use std::env; +// // +// // #[tokio::test] +// // async fn test_kms_wallet_with_localstack() -> anyhow::Result<()> { +// // // Start LocalStack +// // let kms = KmsTestContainer::default().with_show_logs(true); +// // let kms_process = kms.start().await?; +// // +// // // Create a new KMS key +// // let test_key = kms_process.create_key().await?; +// // +// // // Set required environment variables +// // env::set_var("AWS_ACCESS_KEY_ID", "test"); +// // env::set_var("AWS_SECRET_ACCESS_KEY", "test"); +// // env::set_var("AWS_REGION", "us-east-1"); +// // env::set_var("AWS_ENDPOINT_URL", test_key.url); +// // +// // // Create KMS wallet +// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; +// // +// // // Test signing +// // let message = Message::new([1u8; 32]); +// // let signature = wallet.sign(message).await?; +// // +// // // Verify the signature +// // let public_key = wallet.address().hash(); +// // // assert!(signature.verify(&message, &public_key)); +// // +// // Ok(()) +// // } +// // +// // #[tokio::test] +// // async fn test_multiple_signatures() -> anyhow::Result<()> { +// // let kms = KmsTestContainer::default().with_show_logs(false); +// // let kms_process = kms.start().await?; +// // let test_key = kms_process.create_key().await?; +// // +// // env::set_var("AWS_ACCESS_KEY_ID", "test"); +// // env::set_var("AWS_SECRET_ACCESS_KEY", "test"); +// // env::set_var("AWS_REGION", "us-east-1"); +// // env::set_var("AWS_ENDPOINT_URL", test_key.url); +// // +// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; +// // +// // // Sign multiple messages +// // for i in 0..5 { +// // let message = Message::new([i as u8; 32]); +// // let signature = wallet.sign(message).await?; +// // // assert!(signature.verify(&message, &wallet.address().hash())); +// // } +// // +// // Ok(()) +// // } +// // +// // #[tokio::test] +// // async fn test_error_handling() -> anyhow::Result<()> { +// // // Start LocalStack +// // let kms = KmsTestContainer::default().with_show_logs(false); +// // let kms_process = kms.start().await?; +// // +// // // Set required environment variables first +// // std::env::set_var("AWS_ACCESS_KEY_ID", "test"); +// // std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); +// // std::env::set_var("AWS_REGION", "us-east-1"); +// // std::env::set_var("AWS_ENDPOINT_URL", &kms_process.url); +// // +// // // Test 1: Invalid key ID +// // { +// // let result = KMSWallet::from_kms_key_id("invalid-key-id".to_string(), None).await; +// // dbg!(&result); +// // assert!(result.is_err()); +// // let err = result.unwrap_err().to_string(); +// // println!("Invalid key ID error: {}", err); +// // assert!(err.contains("AWS KMS Error")); // Check for our error prefix +// // } +// // +// // // Test 2: Wrong key spec +// // { +// // let response = kms_process +// // .client +// // .create_key() +// // .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) +// // .key_spec(aws_sdk_kms::types::KeySpec::Rsa2048) // Wrong key spec +// // .send() +// // .await?; +// // +// // let key_id = response +// // .key_metadata +// // .and_then(|metadata| metadata.arn) +// // .ok_or_else(|| anyhow::anyhow!("Key ARN missing from response"))?; +// // +// // let result = KMSWallet::from_kms_key_id(key_id, None).await; +// // println!("Wrong key spec error: {:?}", result); +// // assert!(result.is_err()); +// // let err = result.unwrap_err().to_string(); +// // assert!(err.contains("Invalid key type") || err.contains("key_spec")); +// // } +// // +// // // Test 3: Invalid endpoint +// // { +// // // Set invalid endpoint +// // std::env::set_var("AWS_ENDPOINT_URL", "http://invalid-endpoint:4566"); +// // +// // let result = KMSWallet::from_kms_key_id("any-key-id".to_string(), None).await; +// // assert!(result.is_err()); +// // let err = result.unwrap_err().to_string(); +// // println!("Invalid endpoint error: {}", err); +// // assert!(err.contains("AWS KMS Error") || err.contains("endpoint")); +// // } +// // +// // Ok(()) +// // } +// // +// // // Helper function to print environment variables (useful for debugging) +// // fn print_aws_env() { +// // println!("AWS Environment Variables:"); +// // println!("AWS_ACCESS_KEY_ID: {:?}", env::var("AWS_ACCESS_KEY_ID")); +// // println!("AWS_SECRET_ACCESS_KEY: {:?}", env::var("AWS_SECRET_ACCESS_KEY")); +// // println!("AWS_REGION: {:?}", env::var("AWS_REGION")); +// // println!("AWS_ENDPOINT_URL: {:?}", env::var("AWS_ENDPOINT_URL")); +// // } +// // } +// +// +// #[cfg(test)] +// mod tests { +// use super::*; +// use fuel_crypto::{Message, PublicKey}; +// use std::env; +// use std::str::FromStr; +// +// // Helper function to set up test environment +// async fn setup_test_environment() -> anyhow::Result<(KmsTestProcess, KmsTestKey)> { +// let kms = KmsTestContainer::default().with_show_logs(false); +// let kms_process = kms.start().await?; +// let test_key = kms_process.create_key().await?; +// +// // Set required environment variables +// env::set_var("AWS_ACCESS_KEY_ID", "test"); +// env::set_var("AWS_SECRET_ACCESS_KEY", "test"); +// env::set_var("AWS_REGION", "us-east-1"); +// env::set_var("AWS_ENDPOINT_URL", &test_key.url); +// +// Ok((kms_process, test_key)) +// } +// +// #[tokio::test] +// async fn test_wallet_creation_and_basic_operations() -> anyhow::Result<()> { +// let (kms_process,test_key) = setup_test_environment().await?; +// +// // Test wallet creation +// let wallet = AWSWallet::from_kms_key_id(test_key.id, None).await?; +// +// // Verify address format +// assert!(wallet.address().to_string().starts_with("fuel")); +// +// // Verify provider behavior +// assert!(wallet.provider().is_none()); +// assert!(wallet.try_provider().is_err()); +// +// Ok(()) +// } +// +// #[tokio::test] +// async fn test_message_signing_and_verification() -> anyhow::Result<()> { +// let (kms_process, test_key) = setup_test_environment().await?; +// let wallet = AWSWallet::from_kms_key_id(test_key.id, None).await?; +// +// // Test signing with different message types +// let test_cases = vec![ +// [0u8; 32], // Zero message +// [1u8; 32], // All ones +// [255u8; 32], // All max values +// [123u8; 32], // Random values +// ]; +// +// for msg_bytes in test_cases { +// let message = Message::new(msg_bytes); +// let signature = wallet.sign(message).await?; +// +// // Verify the signature using the wallet's public key +// let public_key = PublicKey::from_str(&wallet.address().hash().to_string())?; +// +// // assert!(signature.verify(&message, &public_key)); +// } +// +// Ok(()) +// } +// +// // #[tokio::test] +// // async fn test_concurrent_signing() -> anyhow::Result<()> { +// // let (_, test_key) = setup_test_environment().await?; +// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; +// // +// // // Create multiple signing tasks +// // let mut handles = vec![]; +// // for i in 0..5 { +// // let wallet_clone = wallet.clone(); +// // let handle = tokio::spawn(async move { +// // let message = Message::new([i as u8; 32]); +// // wallet_clone.sign(message).await +// // }); +// // handles.push(handle); +// // } +// // +// // // Wait for all signatures and verify them +// // for handle in handles { +// // let signature = handle.await??; +// // assert!(signature.verify( +// // &Message::new([0u8; 32]), +// // &PublicKey::from_bytes(wallet.address().hash().as_ref())? +// // )); +// // } +// // +// // Ok(()) +// // } +// // +// // #[tokio::test] +// // async fn test_error_cases() -> anyhow::Result<()> { +// // let (kms_process, _) = setup_test_environment().await?; +// // +// // // Test 1: Invalid key ID +// // let result = KMSWallet::from_kms_key_id("invalid-key-id".to_string(), None).await; +// // assert!(result.is_err()); +// // assert!(result.unwrap_err().to_string().contains("AWS KMS Error")); +// // +// // // Test 2: Wrong key spec +// // let response = kms_process +// // .client +// // .create_key() +// // .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) +// // .key_spec(aws_sdk_kms::types::KeySpec::Rsa2048) +// // .send() +// // .await?; +// // +// // let key_id = response +// // .key_metadata +// // .and_then(|metadata| metadata.arn) +// // .ok_or_else(|| anyhow::anyhow!("Key ARN missing from response"))?; +// // +// // let result = KMSWallet::from_kms_key_id(key_id, None).await; +// // assert!(result.is_err()); +// // assert!(result.unwrap_err().to_string().contains("Invalid key type")); +// // +// // // Test 3: Missing environment variables +// // env::remove_var("AWS_REGION"); +// // let result = KMSWallet::from_kms_key_id("any-key-id".to_string(), None).await; +// // assert!(result.is_err()); +// // +// // Ok(()) +// // } +// // +// // #[tokio::test] +// // async fn test_address_consistency() -> anyhow::Result<()> { +// // let (_, test_key) = setup_test_environment().await?; +// // let wallet = KMSWallet::from_kms_key_id(test_key.id.clone(), None).await?; +// // +// // // Create another wallet instance with the same key +// // let wallet2 = KMSWallet::from_kms_key_id(test_key.id, None).await?; +// // +// // // Verify addresses match +// // assert_eq!(wallet.address(), wallet2.address()); +// // // assert_eq!(wallet.address(), wallet.kms_data.cached_address); +// // +// // Ok(()) +// // } +// // +// // +// // +// // #[tokio::test] +// // async fn test_asset_inputs() -> anyhow::Result<()> { +// // let (_, test_key) = setup_test_environment().await?; +// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; +// // +// // // Test getting asset inputs (should fail without provider) +// // let result = wallet +// // .get_asset_inputs_for_amount(AssetId::default(), 100, None) +// // .await; +// // assert!(result.is_err()); +// // +// // Ok(()) +// // } +// } diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 7bd1b1316..9864d2481 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -12,6 +12,9 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; +#[cfg(feature = "std")] +pub mod aws_signer; + #[cfg(feature = "coin-cache")] mod coin_cache; From 95aaa27b852b93a754524357038d5879e2613db7 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Wed, 12 Feb 2025 12:35:03 +0100 Subject: [PATCH 02/34] fund kms wallet --- e2e/src/fuel_node.rs | 39 ++++++++++++++---------------- e2e/src/lib.rs | 22 +++-------------- packages/fuels-accounts/Cargo.toml | 2 +- 3 files changed, 22 insertions(+), 41 deletions(-) diff --git a/e2e/src/fuel_node.rs b/e2e/src/fuel_node.rs index 9653a6f9e..327ea0b93 100644 --- a/e2e/src/fuel_node.rs +++ b/e2e/src/fuel_node.rs @@ -8,11 +8,12 @@ use fuel_core_types::{ fuel_types::Address, }; use fuels::crypto::{PublicKey, SecretKey}; -use fuels::prelude::{Provider, WalletUnlocked}; +use fuels::prelude::{Bech32Address, Provider, TxPolicies, WalletUnlocked}; use itertools::Itertools; use rand::Rng; use std::path::PathBuf; use std::str::FromStr; +use anyhow::Context; use url::Url; use fuels::accounts::Account; use fuels::accounts::aws_signer::AwsWallet; @@ -172,35 +173,31 @@ impl FuelNodeProcess { pub async fn fund( &self, - address: Address, - amount: U256 + address: Bech32Address, + amount: u64 ) -> anyhow::Result<()> { - let fuels_provider = Provider::connect(self.url()).await.unwrap(); + let fuels_provider = Provider::connect(self.url()).await?; let mut default_wallet = WalletUnlocked::new_from_private_key( SecretKey::from_str( "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c", ) - .unwrap(), + ?, None, ); - default_wallet.set_provider(fuels_provider.clone()); - // let wallet = AwsWallet::from_kms_key_id(key.id, provider).await?; - - // default_wallet - // .transfer( - // wallet.address(), - // 1_000_000_000, // Amount to transfer - // AssetId::default(), // Using the base asset - // TxParams::default(), - // ) - // .await?; - // if succeeded { - // Ok(()) - // } else { - // Err(anyhow::anyhow!("Failed to fund address {address}")) - // } + + default_wallet + .transfer( + &address, + amount, // Amount to transfer + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07").expect("AssetId to be well formed"), + TxPolicies::default(), + ) + .await.context("Failed to transfer funds")?; + + self.client().produce_blocks(1).await?; + Ok(()) } } diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 8abe2157b..9f13caf15 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -9,8 +9,6 @@ mod kms; mod tests { use crate::fuel_node::FuelNode; use anyhow::Result; - use fuel_core_client::client::schema::schema::__fields::GasCosts::tro; - use fuels::types::U256; use crate::kms::Kms; #[tokio::test(flavor = "multi_thread")] @@ -18,24 +16,10 @@ mod tests { let kms = Kms::default().with_show_logs(false).start().await?; let key = kms.create_key().await?; - let fuel_node = FuelNode::default().with_show_logs(false).start().await?; + let fuel_node_process = FuelNode::default().with_show_logs(false).start().await?; + + fuel_node_process.fund(key.kms_data.address, 5_000_000_000).await?; - fuel_node.fund(key.kms_data.address.into(), U256::from(10)).await?; - - // let g = Url::parse("http://localhost:4000").unwrap(); - - // let fuels_provider = Provider::connect(self.url()).await.unwrap(); - // let fuels_provider = Provider::connect(g).await.unwrap(); - // - // let mut wallet = WalletUnlocked::new_from_private_key( - // SecretKey::from_str("0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c").unwrap(), - // None, - // ); - // - // wallet.set_provider(fuels_provider.clone()); - // - // dbg!(fuels_provider.get_balances(wallet.address()).await?); - // dbg!(fuels_provider.url()); Ok(()) diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 574dd286d..8c74ae876 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -34,7 +34,7 @@ k256 = { workspace = true, features = ["ecdsa-core"] } aws-config = { workspace = true, features = [ "behavior-version-latest", ] } -aws-sdk-kms = { workspace = true } +aws-sdk-kms = { workspace = true, features = ["default"] } [dev-dependencies] mockall = { workspace = true, default-features = false } From db5dd0a4a9edfa4f8eeb136f94875b47517bff4f Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Wed, 12 Feb 2025 16:29:02 +0100 Subject: [PATCH 03/34] add tests --- e2e/Cargo.toml | 3 + e2e/src/client.rs | 30 +- e2e/src/e2e_helpers.rs | 22 + e2e/src/fuel_node.rs | 128 +---- e2e/src/kms.rs | 38 +- e2e/src/lib.rs | 78 ++- packages/fuels-accounts/src/aws.rs | 5 + packages/fuels-accounts/src/aws/aws_client.rs | 63 ++ packages/fuels-accounts/src/aws/aws_signer.rs | 250 ++++++++ packages/fuels-accounts/src/aws_signer.rs | 539 ------------------ packages/fuels-accounts/src/lib.rs | 4 +- 11 files changed, 438 insertions(+), 722 deletions(-) create mode 100644 e2e/src/e2e_helpers.rs create mode 100644 packages/fuels-accounts/src/aws.rs create mode 100644 packages/fuels-accounts/src/aws/aws_client.rs create mode 100644 packages/fuels-accounts/src/aws/aws_signer.rs delete mode 100644 packages/fuels-accounts/src/aws_signer.rs diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index cecc1d7f9..1013d1415 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -33,6 +33,7 @@ fuel-core-chain-config = { workspace = true, features = [ "std", "test-helpers", ] } +fuels-accounts = { workspace = true, features = ["test-helpers"] } fuel-core-types = { workspace = true, features = [ "da-compression", @@ -46,10 +47,12 @@ fuel-core-client = { workspace = true } + [build-dependencies] anyhow = { workspace = true, features = ["std"] } flate2 = { workspace = true, features = ["zlib"] } fuels-accounts = { workspace = true, features = ["std"] } + reqwest = { workspace = true, features = ["blocking", "default-tls"] } semver = { workspace = true } tar = { workspace = true } diff --git a/e2e/src/client.rs b/e2e/src/client.rs index 6771eec54..7cf86eef9 100644 --- a/e2e/src/client.rs +++ b/e2e/src/client.rs @@ -1,10 +1,6 @@ use url::Url; -use fuel_core_client::client::types::CoinType; -use fuel_core_client::client::{types::Block, FuelClient}; -use fuel_core_types::fuel_tx::Transaction; -use fuel_core_types::fuel_types::{Address, AssetId}; -use fuels::types::coin::Coin; +use fuel_core_client::client::FuelClient; use fuels::types::errors::Error; use fuels::types::errors::Result; #[derive(Clone)] @@ -28,30 +24,6 @@ impl HttpClient { Ok(()) } - pub async fn send_tx(&self, tx: &Transaction) -> Result<()> { - self.client - .submit_and_await_commit(tx) - .await - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) - } - - pub async fn get_coin(&self, address: Address, asset_id: AssetId) -> Result { - let coin_type = self - .client - .coins_to_spend(&address, vec![(asset_id, 1, None)], None) - .await - .map_err(|e| Error::Other(e.to_string()))?[0][0]; - - let coin = match coin_type { - CoinType::Coin(c) => Ok(c), - _ => Err(Error::Other("Couldn't get coin".to_string())), - }?; - - Ok(Coin::from(coin)) - } - pub async fn health(&self) -> Result { match self.client.health().await { Ok(healthy) => Ok(healthy), diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs new file mode 100644 index 000000000..c5cda7005 --- /dev/null +++ b/e2e/src/e2e_helpers.rs @@ -0,0 +1,22 @@ +use crate::{ + fuel_node::{FuelNode, FuelNodeProcess}, + kms::{Kms, KmsKey, KmsProcess}, +}; + +pub async fn start_kms(logs: bool) -> anyhow::Result { + Kms::default().with_show_logs(logs).start().await +} +pub async fn create_and_fund_kms_keys( + kms: &KmsProcess, + fuel_node: &FuelNodeProcess, +) -> anyhow::Result { + let amount = 5_000_000_000; + let key = kms.create_key().await?; + let address = key.kms_data.address.clone(); + fuel_node.fund(address, amount).await?; + + Ok(key) +} +pub async fn start_fuel_node(logs: bool) -> anyhow::Result { + FuelNode::default().with_show_logs(logs).start().await +} diff --git a/e2e/src/fuel_node.rs b/e2e/src/fuel_node.rs index 327ea0b93..e816e145e 100644 --- a/e2e/src/fuel_node.rs +++ b/e2e/src/fuel_node.rs @@ -1,23 +1,13 @@ use crate::client::HttpClient; -use fuel_core_chain_config::{ - ChainConfig, CoinConfig, ConsensusConfig, SnapshotWriter, StateConfig, -}; +use anyhow::Context; use fuel_core_types::{ - fuel_crypto::SecretKey as FuelSecretKey, - fuel_tx::{AssetId, Finalizable, Input, Output, TransactionBuilder, TxPointer}, - fuel_types::Address, + fuel_tx::AssetId, }; -use fuels::crypto::{PublicKey, SecretKey}; +use fuels::accounts::Account; +use fuels::crypto::SecretKey; use fuels::prelude::{Bech32Address, Provider, TxPolicies, WalletUnlocked}; -use itertools::Itertools; -use rand::Rng; -use std::path::PathBuf; use std::str::FromStr; -use anyhow::Context; use url::Url; -use fuels::accounts::Account; -use fuels::accounts::aws_signer::AwsWallet; -use fuels::types::U256; #[derive(Default, Debug)] pub struct FuelNode { @@ -30,52 +20,6 @@ pub struct FuelNodeProcess { } impl FuelNode { - fn create_state_config( - path: impl Into, - consensus_key: &PublicKey, - num_wallets: usize, - ) -> anyhow::Result> { - let chain_config = ChainConfig { - consensus: ConsensusConfig::PoA { - signing_key: Input::owner(consensus_key), - }, - ..ChainConfig::local_testnet() - }; - - let mut rng = &mut rand::thread_rng(); - let keys = std::iter::repeat_with(|| FuelSecretKey::random(&mut rng)) - .take(num_wallets) - .collect_vec(); - - let coins = keys - .iter() - .flat_map(|key| { - std::iter::repeat_with(|| CoinConfig { - owner: Input::owner(&key.public_key()), - amount: u64::MAX, - asset_id: AssetId::zeroed(), - tx_id: rng.gen(), - output_index: rng.gen(), - ..Default::default() - }) - .take(10) - .collect_vec() - }) - .collect_vec(); - - let state_config = StateConfig { - coins, - ..StateConfig::local_testnet() - }; - - let snapshot = SnapshotWriter::json(path); - snapshot - .write_state_config(state_config, &chain_config) - .map_err(|_| anyhow::anyhow!("Failed to write state config"))?; - - Ok(keys) - } - pub async fn start(&self) -> anyhow::Result { let unused_port = portpicker::pick_unused_port() .ok_or_else(|| anyhow::anyhow!("No free port to start fuel-core"))?; @@ -120,45 +64,6 @@ impl FuelNodeProcess { HttpClient::new(&self.url) } - async fn send_transfer_tx(client: HttpClient, key: FuelSecretKey) -> anyhow::Result<()> { - let mut tx = TransactionBuilder::script(vec![], vec![]); - - tx.script_gas_limit(1_000_000); - - let secret_key = key; - let address = Input::owner(&secret_key.public_key()); - - let base_asset = AssetId::zeroed(); - let coin = client.get_coin(address, base_asset).await?; - - tx.add_unsigned_coin_input( - secret_key, - coin.utxo_id, - coin.amount, - coin.asset_id, - TxPointer::default(), - ); - - const AMOUNT: u64 = 1; - let to = Address::default(); - tx.add_output(Output::Coin { - to, - amount: AMOUNT, - asset_id: base_asset, - }); - tx.add_output(Output::Change { - to: address, - amount: 0, - asset_id: base_asset, - }); - - let tx = tx.finalize(); - - client.send_tx(&tx.into()).await?; - - Ok(()) - } - async fn wait_until_healthy(&self) { loop { if let Ok(true) = self.client().health().await { @@ -171,30 +76,27 @@ impl FuelNodeProcess { &self.url } - pub async fn fund( - &self, - address: Bech32Address, - amount: u64 - ) -> anyhow::Result<()> { + pub async fn fund(&self, address: Bech32Address, amount: u64) -> anyhow::Result<()> { let fuels_provider = Provider::connect(self.url()).await?; + // Create a wallet with the private key of the default account let mut default_wallet = WalletUnlocked::new_from_private_key( SecretKey::from_str( "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c", - ) - ?, + )?, None, ); default_wallet.set_provider(fuels_provider.clone()); + // Transfer ETH funds to the AWS wallet from the default wallet + let asset_id = + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") + .expect("AssetId to be well formed"); + default_wallet - .transfer( - &address, - amount, // Amount to transfer - AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07").expect("AssetId to be well formed"), - TxPolicies::default(), - ) - .await.context("Failed to transfer funds")?; + .transfer(&address, amount, asset_id, TxPolicies::default()) + .await + .context("Failed to transfer funds")?; self.client().produce_blocks(1).await?; diff --git a/e2e/src/kms.rs b/e2e/src/kms.rs index 49a5bb890..895e816dd 100644 --- a/e2e/src/kms.rs +++ b/e2e/src/kms.rs @@ -1,10 +1,8 @@ use anyhow::Context; -use aws_config::Region; -use aws_sdk_kms::{config::Credentials, Client as AWSClient}; -use aws_sdk_kms::config::BehaviorVersion; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; -use fuels::accounts::aws_signer::KmsData; +use fuels::accounts::aws::{AwsClient, AwsConfig, KmsData}; + #[derive(Default)] pub struct Kms { show_logs: bool, @@ -49,28 +47,8 @@ impl Kms { let port = container.get_host_port_ipv4(4566).await?; let url = format!("http://localhost:{}", port); - // Configure AWS SDK - // let config = aws_config::from_env() - // .endpoint_url(url.clone()) - // .region("us-east-1") - // .credentials_provider(Credentials::new("test", "test", None, None, "test")) - // .load() - // .await; - - let config = aws_config::defaults(BehaviorVersion::latest()) - .credentials_provider(Credentials::new( - "test", - "test", - None, - None, - "Static Credentials", - )) - .endpoint_url(url.clone()) - .region(Region::new("us-east-1")) // placeholder region for test - .load() - .await; - - let client = AWSClient::new(&config); + let config = AwsConfig::for_testing(url.clone()).await; + let client = AwsClient::new(config); Ok(KmsProcess { _container: container, @@ -121,14 +99,15 @@ fn spawn_log_printer(container: &testcontainers::ContainerAsync) { pub struct KmsProcess { _container: testcontainers::ContainerAsync, - client: AWSClient, + client: AwsClient, url: String, } impl KmsProcess { pub async fn create_key(&self) -> anyhow::Result { let response = self - .client + .client. + inner() .create_key() .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) .key_spec(aws_sdk_kms::types::KeySpec::EccSecgP256K1) @@ -145,13 +124,12 @@ impl KmsProcess { Ok(KmsKey { id, - kms_data,// todo fix this + kms_data, url: self.url.clone(), }) } } - #[derive(Debug, Clone)] pub struct KmsKey { pub id: String, diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 9f13caf15..d10c41f3e 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -4,24 +4,84 @@ mod client; mod fuel_node; #[cfg(test)] mod kms; +#[cfg(test)] +mod e2e_helpers; + #[cfg(test)] mod tests { - use crate::fuel_node::FuelNode; use anyhow::Result; - use crate::kms::Kms; + use fuels::prelude::{AssetId, Provider}; + use std::str::FromStr; + use fuels_accounts::aws::AwsWallet; + use fuels_accounts::ViewOnlyAccount; + use crate::e2e_helpers::{create_and_fund_kms_keys, start_fuel_node, start_kms}; - #[tokio::test(flavor = "multi_thread")] - async fn aws_wallet() -> Result<()> { + #[tokio::test] + async fn fund_aws_wallet() -> Result<()> { + let kms = start_kms(false).await?; + let fuel_node = start_fuel_node(false).await?; + let kms_key = create_and_fund_kms_keys(&kms, &fuel_node).await?; - let kms = Kms::default().with_show_logs(false).start().await?; - let key = kms.create_key().await?; - let fuel_node_process = FuelNode::default().with_show_logs(false).start().await?; - - fuel_node_process.fund(key.kms_data.address, 5_000_000_000).await?; + std::env::set_var("AWS_ACCESS_KEY_ID", "test"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); + std::env::set_var("AWS_REGION", "us-east-1"); + std::env::set_var("AWS_ENDPOINT_URL", &kms_key.url); + let asset_id = + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") + .expect("AssetId to be well formed"); + let provider = Provider::connect(fuel_node.url()).await?; + let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; + let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + assert_eq!(founded_coins, 5000000000); Ok(()) } + + #[tokio::test] + async fn deploy_contract() -> anyhow::Result<()> { + use fuels::prelude::*; + + let kms = start_kms(false).await?; + let fuel_node = start_fuel_node(false).await?; + let kms_key = create_and_fund_kms_keys(&kms, &fuel_node).await?; + + std::env::set_var("AWS_ACCESS_KEY_ID", "test"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); + std::env::set_var("AWS_REGION", "us-east-1"); + std::env::set_var("AWS_ENDPOINT_URL", &kms_key.url); + + let asset_id = + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") + .expect("AssetId to be well formed"); + + let provider = Provider::connect(fuel_node.url()).await?; + let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; + + let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + assert_eq!(founded_coins, 5000000000); + + let contract_id = Contract::load_from( + "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", + LoadConfiguration::default(), + )? + .deploy(&wallet, TxPolicies::default()) + .await?; + + println!("Contract deployed @ {contract_id}"); + + let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + assert_eq!(founded_coins, 4999983198); + + Ok(()) + } + + + + + + + } diff --git a/packages/fuels-accounts/src/aws.rs b/packages/fuels-accounts/src/aws.rs new file mode 100644 index 000000000..c69c7182d --- /dev/null +++ b/packages/fuels-accounts/src/aws.rs @@ -0,0 +1,5 @@ +mod aws_client; +mod aws_signer; + +pub use aws_client::*; +pub use aws_signer::*; \ No newline at end of file diff --git a/packages/fuels-accounts/src/aws/aws_client.rs b/packages/fuels-accounts/src/aws/aws_client.rs new file mode 100644 index 000000000..189b78dac --- /dev/null +++ b/packages/fuels-accounts/src/aws/aws_client.rs @@ -0,0 +1,63 @@ +use aws_config::{default_provider::credentials::DefaultCredentialsChain, Region, SdkConfig}; +#[cfg(feature = "test-helpers")] +use aws_sdk_kms::config::Credentials; +use aws_sdk_kms::{config::BehaviorVersion, Client}; + +#[derive(Debug, Clone)] +pub struct AwsConfig { + sdk_config: SdkConfig, +} + +impl AwsConfig { + pub async fn from_env() -> Self { + let loader = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(DefaultCredentialsChain::builder().build().await); + + Self { + sdk_config: loader.load().await, + } + } + + #[cfg(feature = "test-helpers")] + 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() + } +} + +#[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 + } +} diff --git a/packages/fuels-accounts/src/aws/aws_signer.rs b/packages/fuels-accounts/src/aws/aws_signer.rs new file mode 100644 index 000000000..6af803137 --- /dev/null +++ b/packages/fuels-accounts/src/aws/aws_signer.rs @@ -0,0 +1,250 @@ +use aws_sdk_kms::{ + primitives::Blob, + types::{KeySpec, MessageType, SigningAlgorithmSpec}, + // Client as AwsClient, +}; +use fuel_crypto::{Message, PublicKey, Signature}; +use fuel_types::AssetId; +use fuels_core::{ + traits::Signer, + types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + coin_type_id::CoinTypeId, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + }, +}; +use k256::{ + ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}, + pkcs8::DecodePublicKey, + PublicKey as K256PublicKey, +}; + +use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; +use crate::aws::{AwsClient, AwsConfig}; + +const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; + +#[derive(Clone, Debug)] +pub struct AwsWallet { + wallet: Wallet, + kms_data: KmsData, +} + +#[derive(Clone, Debug)] +pub struct KmsData { + id: String, + client: AwsClient, + pub public_key: Vec, + pub address: Bech32Address, +} + +impl KmsData { + pub async fn new(id: String, client: AwsClient) -> anyhow::Result { + Self::validate_key_type(&client, &id).await?; + let public_key = Self::fetch_public_key(&client, &id).await?; + let address = Self::create_bech32_address(&public_key)?; + + Ok(Self { + id, + client, + public_key, + address, + }) + } + + async fn validate_key_type(client: &AwsClient, key_id: &str) -> anyhow::Result<()> { + let key_spec = client + .inner() + .get_public_key() + .key_id(key_id) + .send() + .await? + .key_spec; + + match key_spec { + Some(KeySpec::EccSecgP256K1) => Ok(()), + other => anyhow::bail!( + "{}: Invalid key type, expected EccSecgP256K1, got {:?}", + AWS_KMS_ERROR_PREFIX, + other + ), + } + } + + async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { + let response = client.inner().get_public_key().key_id(key_id).send().await?; + + let public_key = response + .public_key() + .ok_or_else(|| anyhow::anyhow!("{}: No public key returned", AWS_KMS_ERROR_PREFIX))?; + + Ok(public_key.clone().into_inner()) + } + + fn create_bech32_address(public_key_bytes: &[u8]) -> anyhow::Result { + let k256_public_key = K256PublicKey::from_public_key_der(public_key_bytes) + .map_err(|_| anyhow::anyhow!("{}: Invalid DER public key", AWS_KMS_ERROR_PREFIX))?; + + let public_key = PublicKey::from(k256_public_key); + let fuel_address = public_key.hash(); + + Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_address)) + } + + async fn sign_message(&self, message: Message) -> Result { + let signature_der = self.request_signature(message).await?; + let (signature, recovery_id) = self.process_signature(&signature_der, message)?; + + Ok(self.create_fuel_signature(signature, recovery_id)) + } + + async fn request_signature(&self, message: Message) -> Result> { + let reply = self + .client + .inner() + .sign() + .key_id(&self.id) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .message_type(MessageType::Digest) + .message(Blob::new(*message)) + .send() + .await + .map_err(|e| { + Error::Other(format!("{}: Failed to sign: {:?}", AWS_KMS_ERROR_PREFIX, e)) + })?; + + reply + .signature + .map(|sig| sig.into_inner()) + .ok_or_else(|| Error::Other(format!("{}: No signature returned", AWS_KMS_ERROR_PREFIX))) + } + + fn process_signature( + &self, + signature_der: &[u8], + message: Message, + ) -> Result<(K256Signature, RecoveryId)> { + let sig = K256Signature::from_der(signature_der).map_err(|_| { + Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) + })?; + let sig = sig.normalize_s().unwrap_or(sig); + + let recovery_id = self.determine_recovery_id(&sig, message)?; + + Ok((sig, recovery_id)) + } + + fn determine_recovery_id(&self, sig: &K256Signature, message: Message) -> Result { + let recid1 = RecoveryId::new(false, false); + let recid2 = RecoveryId::new(true, false); + + let correct_public_key = K256PublicKey::from_public_key_der(&self.public_key) + .map_err(|_| { + Error::Other(format!( + "{}: Invalid cached public key", + AWS_KMS_ERROR_PREFIX + )) + })? + .into(); + + let rec1 = VerifyingKey::recover_from_prehash(&*message, sig, recid1); + let rec2 = VerifyingKey::recover_from_prehash(&*message, sig, recid2); + + if rec1.map(|r| r == correct_public_key).unwrap_or(false) { + Ok(recid1) + } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { + Ok(recid2) + } else { + Err(Error::Other(format!( + "{}: Invalid signature (reduced-x form coordinate)", + AWS_KMS_ERROR_PREFIX + ))) + } + } + + fn create_fuel_signature( + &self, + signature: K256Signature, + recovery_id: RecoveryId, + ) -> Signature { + debug_assert!( + !recovery_id.is_x_reduced(), + "reduced-x form coordinates should be caught earlier" + ); + + let v = recovery_id.is_y_odd() as u8; + let mut signature_bytes = <[u8; 64]>::from(signature.to_bytes()); + signature_bytes[32] = (v << 7) | (signature_bytes[32] & 0x7f); + + Signature::from_bytes(signature_bytes) + } +} + +impl AwsWallet { + pub async fn from_kms_key_id( + kms_key_id: String, + provider: Option, + ) -> anyhow::Result { + let config = AwsConfig::from_env().await; + let client = AwsClient::new(config); + let kms_data = KmsData::new(kms_key_id, client).await?; + + Ok(Self { + wallet: Wallet::from_address(kms_data.address.clone(), provider), + kms_data, + }) + } + + pub fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + pub fn provider(&self) -> Option<&Provider> { + self.wallet.provider() + } +} + +#[async_trait::async_trait] +impl Signer for AwsWallet { + async fn sign(&self, message: Message) -> Result { + self.kms_data.sign_message(message).await + } + + fn address(&self) -> &Bech32Address { + &self.kms_data.address + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl ViewOnlyAccount for AwsWallet { + fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + fn try_provider(&self) -> Result<&Provider> { + self.wallet.provider().ok_or_else(|| { + Error::Other("No provider available. Make sure to use `set_provider`".to_owned()) + }) + } + + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + excluded_coins: Option>, + ) -> Result> { + self.wallet + .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) + .await + } +} + +#[async_trait::async_trait] +impl Account for AwsWallet { + fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { + tb.add_signer(self.clone())?; + Ok(()) + } +} \ No newline at end of file diff --git a/packages/fuels-accounts/src/aws_signer.rs b/packages/fuels-accounts/src/aws_signer.rs deleted file mode 100644 index 6e5791215..000000000 --- a/packages/fuels-accounts/src/aws_signer.rs +++ /dev/null @@ -1,539 +0,0 @@ -use aws_sdk_kms::{ - primitives::Blob, - types::{KeySpec, MessageType, SigningAlgorithmSpec}, - Client as AwsClient, -}; -use fuel_crypto::{Message, PublicKey, Signature}; -use fuel_types::AssetId; -use fuels_core::{ - traits::Signer, - types::{ - bech32::{Bech32Address, FUEL_BECH32_HRP}, - coin_type_id::CoinTypeId, - errors::{Error, Result}, - input::Input, - transaction_builders::TransactionBuilder, - }, -}; -use k256::{ - ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}, - pkcs8::DecodePublicKey, - PublicKey as K256PublicKey, -}; - -use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; - -const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; - -#[derive(Clone, Debug)] -pub struct AwsWallet { - wallet: Wallet, - kms_data: KmsData, -} - -#[derive(Clone, Debug)] -pub struct KmsData { - id: String, - client: AwsClient, - public_key: Vec, - pub address: Bech32Address, -} - -impl KmsData { - pub async fn new(id: String, client: AwsClient) -> anyhow::Result { - Self::validate_key_type(&client, &id).await?; - let public_key = Self::fetch_public_key(&client, &id).await?; - let address = Self::create_bech32_address(&public_key)?; - - Ok(Self { - id, - client, - public_key, - address, - }) - } - - async fn validate_key_type(client: &AwsClient, key_id: &str) -> anyhow::Result<()> { - let key_spec = client - .get_public_key() - .key_id(key_id) - .send() - .await? - .key_spec; - - match key_spec { - Some(KeySpec::EccSecgP256K1) => Ok(()), - other => anyhow::bail!( - "{}: Invalid key type, expected EccSecgP256K1, got {:?}", - AWS_KMS_ERROR_PREFIX, - other - ), - } - } - - async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { - let response = client.get_public_key().key_id(key_id).send().await?; - - let public_key = response - .public_key() - .ok_or_else(|| anyhow::anyhow!("{}: No public key returned", AWS_KMS_ERROR_PREFIX))?; - - Ok(public_key.clone().into_inner()) - } - - fn create_bech32_address(public_key_bytes: &[u8]) -> anyhow::Result { - let k256_public_key = K256PublicKey::from_public_key_der(public_key_bytes) - .map_err(|_| anyhow::anyhow!("{}: Invalid DER public key", AWS_KMS_ERROR_PREFIX))?; - - let public_key = PublicKey::from(k256_public_key); - let fuel_address = public_key.hash(); - - Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_address)) - } - - async fn sign_message(&self, message: Message) -> Result { - let signature_der = self.request_signature(message).await?; - let (signature, recovery_id) = self.process_signature(&signature_der, message)?; - - Ok(self.create_fuel_signature(signature, recovery_id)) - } - - async fn request_signature(&self, message: Message) -> Result> { - let reply = self - .client - .sign() - .key_id(&self.id) - .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) - .message_type(MessageType::Digest) - .message(Blob::new(*message)) - .send() - .await - .map_err(|e| { - Error::Other(format!("{}: Failed to sign: {:?}", AWS_KMS_ERROR_PREFIX, e)) - })?; - - reply - .signature - .map(|sig| sig.into_inner()) - .ok_or_else(|| Error::Other(format!("{}: No signature returned", AWS_KMS_ERROR_PREFIX))) - } - - fn process_signature( - &self, - signature_der: &[u8], - message: Message, - ) -> Result<(K256Signature, RecoveryId)> { - let sig = K256Signature::from_der(signature_der).map_err(|_| { - Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) - })?; - let sig = sig.normalize_s().unwrap_or(sig); - - let recovery_id = self.determine_recovery_id(&sig, message)?; - - Ok((sig, recovery_id)) - } - - fn determine_recovery_id(&self, sig: &K256Signature, message: Message) -> Result { - let recid1 = RecoveryId::new(false, false); - let recid2 = RecoveryId::new(true, false); - - let correct_public_key = K256PublicKey::from_public_key_der(&self.public_key) - .map_err(|_| { - Error::Other(format!( - "{}: Invalid cached public key", - AWS_KMS_ERROR_PREFIX - )) - })? - .into(); - - let rec1 = VerifyingKey::recover_from_prehash(&*message, sig, recid1); - let rec2 = VerifyingKey::recover_from_prehash(&*message, sig, recid2); - - if rec1.map(|r| r == correct_public_key).unwrap_or(false) { - Ok(recid1) - } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { - Ok(recid2) - } else { - Err(Error::Other(format!( - "{}: Invalid signature (reduced-x form coordinate)", - AWS_KMS_ERROR_PREFIX - ))) - } - } - - fn create_fuel_signature( - &self, - signature: K256Signature, - recovery_id: RecoveryId, - ) -> Signature { - debug_assert!( - !recovery_id.is_x_reduced(), - "reduced-x form coordinates should be caught earlier" - ); - - let v = recovery_id.is_y_odd() as u8; - let mut signature_bytes = <[u8; 64]>::from(signature.to_bytes()); - signature_bytes[32] = (v << 7) | (signature_bytes[32] & 0x7f); - - Signature::from_bytes(signature_bytes) - } -} - -impl AwsWallet { - pub async fn from_kms_key_id( - kms_key_id: String, - provider: Option, - ) -> anyhow::Result { - let config = aws_config::load_from_env().await; - let client = AwsClient::new(&config); - - let kms_data = KmsData::new(kms_key_id, client).await?; - println!("Fuel address: {}", kms_data.address); - - Ok(Self { - wallet: Wallet::from_address(kms_data.address.clone(), provider), - kms_data, - }) - } - - pub fn address(&self) -> &Bech32Address { - self.wallet.address() - } - - pub fn provider(&self) -> Option<&Provider> { - self.wallet.provider() - } -} - -#[async_trait::async_trait] -impl Signer for AwsWallet { - async fn sign(&self, message: Message) -> Result { - self.kms_data.sign_message(message).await - } - - fn address(&self) -> &Bech32Address { - &self.kms_data.address - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl ViewOnlyAccount for AwsWallet { - fn address(&self) -> &Bech32Address { - self.wallet.address() - } - - fn try_provider(&self) -> Result<&Provider> { - self.wallet.provider().ok_or_else(|| { - Error::Other("No provider available. Make sure to use `set_provider`".to_owned()) - }) - } - - async fn get_asset_inputs_for_amount( - &self, - asset_id: AssetId, - amount: u64, - excluded_coins: Option>, - ) -> Result> { - self.wallet - .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) - .await - } -} - -#[async_trait::async_trait] -impl Account for AwsWallet { - fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { - tb.add_signer(self.clone())?; - Ok(()) - } -} - -// -// // Integration tests using the LocalStack KMS -// // #[cfg(test)] -// // mod tests { -// // use super::*; -// // use fuel_crypto::Message; -// // use std::env; -// // -// // #[tokio::test] -// // async fn test_kms_wallet_with_localstack() -> anyhow::Result<()> { -// // // Start LocalStack -// // let kms = KmsTestContainer::default().with_show_logs(true); -// // let kms_process = kms.start().await?; -// // -// // // Create a new KMS key -// // let test_key = kms_process.create_key().await?; -// // -// // // Set required environment variables -// // env::set_var("AWS_ACCESS_KEY_ID", "test"); -// // env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// // env::set_var("AWS_REGION", "us-east-1"); -// // env::set_var("AWS_ENDPOINT_URL", test_key.url); -// // -// // // Create KMS wallet -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Test signing -// // let message = Message::new([1u8; 32]); -// // let signature = wallet.sign(message).await?; -// // -// // // Verify the signature -// // let public_key = wallet.address().hash(); -// // // assert!(signature.verify(&message, &public_key)); -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_multiple_signatures() -> anyhow::Result<()> { -// // let kms = KmsTestContainer::default().with_show_logs(false); -// // let kms_process = kms.start().await?; -// // let test_key = kms_process.create_key().await?; -// // -// // env::set_var("AWS_ACCESS_KEY_ID", "test"); -// // env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// // env::set_var("AWS_REGION", "us-east-1"); -// // env::set_var("AWS_ENDPOINT_URL", test_key.url); -// // -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Sign multiple messages -// // for i in 0..5 { -// // let message = Message::new([i as u8; 32]); -// // let signature = wallet.sign(message).await?; -// // // assert!(signature.verify(&message, &wallet.address().hash())); -// // } -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_error_handling() -> anyhow::Result<()> { -// // // Start LocalStack -// // let kms = KmsTestContainer::default().with_show_logs(false); -// // let kms_process = kms.start().await?; -// // -// // // Set required environment variables first -// // std::env::set_var("AWS_ACCESS_KEY_ID", "test"); -// // std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// // std::env::set_var("AWS_REGION", "us-east-1"); -// // std::env::set_var("AWS_ENDPOINT_URL", &kms_process.url); -// // -// // // Test 1: Invalid key ID -// // { -// // let result = KMSWallet::from_kms_key_id("invalid-key-id".to_string(), None).await; -// // dbg!(&result); -// // assert!(result.is_err()); -// // let err = result.unwrap_err().to_string(); -// // println!("Invalid key ID error: {}", err); -// // assert!(err.contains("AWS KMS Error")); // Check for our error prefix -// // } -// // -// // // Test 2: Wrong key spec -// // { -// // let response = kms_process -// // .client -// // .create_key() -// // .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) -// // .key_spec(aws_sdk_kms::types::KeySpec::Rsa2048) // Wrong key spec -// // .send() -// // .await?; -// // -// // let key_id = response -// // .key_metadata -// // .and_then(|metadata| metadata.arn) -// // .ok_or_else(|| anyhow::anyhow!("Key ARN missing from response"))?; -// // -// // let result = KMSWallet::from_kms_key_id(key_id, None).await; -// // println!("Wrong key spec error: {:?}", result); -// // assert!(result.is_err()); -// // let err = result.unwrap_err().to_string(); -// // assert!(err.contains("Invalid key type") || err.contains("key_spec")); -// // } -// // -// // // Test 3: Invalid endpoint -// // { -// // // Set invalid endpoint -// // std::env::set_var("AWS_ENDPOINT_URL", "http://invalid-endpoint:4566"); -// // -// // let result = KMSWallet::from_kms_key_id("any-key-id".to_string(), None).await; -// // assert!(result.is_err()); -// // let err = result.unwrap_err().to_string(); -// // println!("Invalid endpoint error: {}", err); -// // assert!(err.contains("AWS KMS Error") || err.contains("endpoint")); -// // } -// // -// // Ok(()) -// // } -// // -// // // Helper function to print environment variables (useful for debugging) -// // fn print_aws_env() { -// // println!("AWS Environment Variables:"); -// // println!("AWS_ACCESS_KEY_ID: {:?}", env::var("AWS_ACCESS_KEY_ID")); -// // println!("AWS_SECRET_ACCESS_KEY: {:?}", env::var("AWS_SECRET_ACCESS_KEY")); -// // println!("AWS_REGION: {:?}", env::var("AWS_REGION")); -// // println!("AWS_ENDPOINT_URL: {:?}", env::var("AWS_ENDPOINT_URL")); -// // } -// // } -// -// -// #[cfg(test)] -// mod tests { -// use super::*; -// use fuel_crypto::{Message, PublicKey}; -// use std::env; -// use std::str::FromStr; -// -// // Helper function to set up test environment -// async fn setup_test_environment() -> anyhow::Result<(KmsTestProcess, KmsTestKey)> { -// let kms = KmsTestContainer::default().with_show_logs(false); -// let kms_process = kms.start().await?; -// let test_key = kms_process.create_key().await?; -// -// // Set required environment variables -// env::set_var("AWS_ACCESS_KEY_ID", "test"); -// env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// env::set_var("AWS_REGION", "us-east-1"); -// env::set_var("AWS_ENDPOINT_URL", &test_key.url); -// -// Ok((kms_process, test_key)) -// } -// -// #[tokio::test] -// async fn test_wallet_creation_and_basic_operations() -> anyhow::Result<()> { -// let (kms_process,test_key) = setup_test_environment().await?; -// -// // Test wallet creation -// let wallet = AWSWallet::from_kms_key_id(test_key.id, None).await?; -// -// // Verify address format -// assert!(wallet.address().to_string().starts_with("fuel")); -// -// // Verify provider behavior -// assert!(wallet.provider().is_none()); -// assert!(wallet.try_provider().is_err()); -// -// Ok(()) -// } -// -// #[tokio::test] -// async fn test_message_signing_and_verification() -> anyhow::Result<()> { -// let (kms_process, test_key) = setup_test_environment().await?; -// let wallet = AWSWallet::from_kms_key_id(test_key.id, None).await?; -// -// // Test signing with different message types -// let test_cases = vec![ -// [0u8; 32], // Zero message -// [1u8; 32], // All ones -// [255u8; 32], // All max values -// [123u8; 32], // Random values -// ]; -// -// for msg_bytes in test_cases { -// let message = Message::new(msg_bytes); -// let signature = wallet.sign(message).await?; -// -// // Verify the signature using the wallet's public key -// let public_key = PublicKey::from_str(&wallet.address().hash().to_string())?; -// -// // assert!(signature.verify(&message, &public_key)); -// } -// -// Ok(()) -// } -// -// // #[tokio::test] -// // async fn test_concurrent_signing() -> anyhow::Result<()> { -// // let (_, test_key) = setup_test_environment().await?; -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Create multiple signing tasks -// // let mut handles = vec![]; -// // for i in 0..5 { -// // let wallet_clone = wallet.clone(); -// // let handle = tokio::spawn(async move { -// // let message = Message::new([i as u8; 32]); -// // wallet_clone.sign(message).await -// // }); -// // handles.push(handle); -// // } -// // -// // // Wait for all signatures and verify them -// // for handle in handles { -// // let signature = handle.await??; -// // assert!(signature.verify( -// // &Message::new([0u8; 32]), -// // &PublicKey::from_bytes(wallet.address().hash().as_ref())? -// // )); -// // } -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_error_cases() -> anyhow::Result<()> { -// // let (kms_process, _) = setup_test_environment().await?; -// // -// // // Test 1: Invalid key ID -// // let result = KMSWallet::from_kms_key_id("invalid-key-id".to_string(), None).await; -// // assert!(result.is_err()); -// // assert!(result.unwrap_err().to_string().contains("AWS KMS Error")); -// // -// // // Test 2: Wrong key spec -// // let response = kms_process -// // .client -// // .create_key() -// // .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) -// // .key_spec(aws_sdk_kms::types::KeySpec::Rsa2048) -// // .send() -// // .await?; -// // -// // let key_id = response -// // .key_metadata -// // .and_then(|metadata| metadata.arn) -// // .ok_or_else(|| anyhow::anyhow!("Key ARN missing from response"))?; -// // -// // let result = KMSWallet::from_kms_key_id(key_id, None).await; -// // assert!(result.is_err()); -// // assert!(result.unwrap_err().to_string().contains("Invalid key type")); -// // -// // // Test 3: Missing environment variables -// // env::remove_var("AWS_REGION"); -// // let result = KMSWallet::from_kms_key_id("any-key-id".to_string(), None).await; -// // assert!(result.is_err()); -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_address_consistency() -> anyhow::Result<()> { -// // let (_, test_key) = setup_test_environment().await?; -// // let wallet = KMSWallet::from_kms_key_id(test_key.id.clone(), None).await?; -// // -// // // Create another wallet instance with the same key -// // let wallet2 = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Verify addresses match -// // assert_eq!(wallet.address(), wallet2.address()); -// // // assert_eq!(wallet.address(), wallet.kms_data.cached_address); -// // -// // Ok(()) -// // } -// // -// // -// // -// // #[tokio::test] -// // async fn test_asset_inputs() -> anyhow::Result<()> { -// // let (_, test_key) = setup_test_environment().await?; -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Test getting asset inputs (should fail without provider) -// // let result = wallet -// // .get_asset_inputs_for_amount(AssetId::default(), 100, None) -// // .await; -// // assert!(result.is_err()); -// // -// // Ok(()) -// // } -// } diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 9864d2481..31b8b9402 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -11,15 +11,15 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; - #[cfg(feature = "std")] -pub mod aws_signer; +pub mod aws; #[cfg(feature = "coin-cache")] mod coin_cache; pub mod predicate; + #[cfg(test)] mod test { #[test] From 5b77e232d0f8c33688f29421f80615b14ecd61ac Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Wed, 12 Feb 2025 17:07:55 +0100 Subject: [PATCH 04/34] fmt --- e2e/src/fuel_node.rs | 4 +- e2e/src/kms.rs | 6 +-- e2e/src/lib.rs | 41 +++++++++++-------- e2e/tests/wallets.rs | 7 ---- packages/fuels-accounts/src/aws.rs | 2 +- packages/fuels-accounts/src/aws/aws_signer.rs | 14 ++++--- packages/fuels-accounts/src/lib.rs | 1 - scripts/change-log/src/get_full_changelog.rs | 2 +- 8 files changed, 39 insertions(+), 38 deletions(-) diff --git a/e2e/src/fuel_node.rs b/e2e/src/fuel_node.rs index e816e145e..081c0cbf2 100644 --- a/e2e/src/fuel_node.rs +++ b/e2e/src/fuel_node.rs @@ -1,8 +1,6 @@ use crate::client::HttpClient; use anyhow::Context; -use fuel_core_types::{ - fuel_tx::AssetId, -}; +use fuel_core_types::fuel_tx::AssetId; use fuels::accounts::Account; use fuels::crypto::SecretKey; use fuels::prelude::{Bech32Address, Provider, TxPolicies, WalletUnlocked}; diff --git a/e2e/src/kms.rs b/e2e/src/kms.rs index 895e816dd..70dd86762 100644 --- a/e2e/src/kms.rs +++ b/e2e/src/kms.rs @@ -1,7 +1,7 @@ use anyhow::Context; +use fuels::accounts::aws::{AwsClient, AwsConfig, KmsData}; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; -use fuels::accounts::aws::{AwsClient, AwsConfig, KmsData}; #[derive(Default)] pub struct Kms { @@ -106,8 +106,8 @@ pub struct KmsProcess { impl KmsProcess { pub async fn create_key(&self) -> anyhow::Result { let response = self - .client. - inner() + .client + .inner() .create_key() .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) .key_spec(aws_sdk_kms::types::KeySpec::EccSecgP256K1) diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index d10c41f3e..2d0eaff00 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -1,21 +1,20 @@ #[cfg(test)] mod client; #[cfg(test)] +mod e2e_helpers; +#[cfg(test)] mod fuel_node; #[cfg(test)] mod kms; -#[cfg(test)] -mod e2e_helpers; - #[cfg(test)] mod tests { + use crate::e2e_helpers::{create_and_fund_kms_keys, start_fuel_node, start_kms}; use anyhow::Result; use fuels::prelude::{AssetId, Provider}; - use std::str::FromStr; use fuels_accounts::aws::AwsWallet; use fuels_accounts::ViewOnlyAccount; - use crate::e2e_helpers::{create_and_fund_kms_keys, start_fuel_node, start_kms}; + use std::str::FromStr; #[tokio::test] async fn fund_aws_wallet() -> Result<()> { @@ -35,7 +34,12 @@ mod tests { let provider = Provider::connect(fuel_node.url()).await?; let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; - let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + let founded_coins = wallet + .get_coins(asset_id) + .await? + .first() + .expect("No coins found") + .amount; assert_eq!(founded_coins, 5000000000); Ok(()) } @@ -60,28 +64,31 @@ mod tests { let provider = Provider::connect(fuel_node.url()).await?; let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; - let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + let founded_coins = wallet + .get_coins(asset_id) + .await? + .first() + .expect("No coins found") + .amount; assert_eq!(founded_coins, 5000000000); let contract_id = Contract::load_from( "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", LoadConfiguration::default(), )? - .deploy(&wallet, TxPolicies::default()) - .await?; + .deploy(&wallet, TxPolicies::default()) + .await?; println!("Contract deployed @ {contract_id}"); - let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + let founded_coins = wallet + .get_coins(asset_id) + .await? + .first() + .expect("No coins found") + .amount; assert_eq!(founded_coins, 4999983198); Ok(()) } - - - - - - - } diff --git a/e2e/tests/wallets.rs b/e2e/tests/wallets.rs index 352756bdd..43ab1d319 100644 --- a/e2e/tests/wallets.rs +++ b/e2e/tests/wallets.rs @@ -557,10 +557,3 @@ async fn wallet_transfer_respects_maturity_and_expiration() -> Result<()> { Ok(()) } - -#[tokio::test] -async fn aws_kms() -> Result<()> { - FuelNode::start().await.expect("TODO: panic message"); - - Ok(()) -} diff --git a/packages/fuels-accounts/src/aws.rs b/packages/fuels-accounts/src/aws.rs index c69c7182d..71491823d 100644 --- a/packages/fuels-accounts/src/aws.rs +++ b/packages/fuels-accounts/src/aws.rs @@ -2,4 +2,4 @@ mod aws_client; mod aws_signer; pub use aws_client::*; -pub use aws_signer::*; \ No newline at end of file +pub use aws_signer::*; diff --git a/packages/fuels-accounts/src/aws/aws_signer.rs b/packages/fuels-accounts/src/aws/aws_signer.rs index 6af803137..462d318f3 100644 --- a/packages/fuels-accounts/src/aws/aws_signer.rs +++ b/packages/fuels-accounts/src/aws/aws_signer.rs @@ -1,7 +1,6 @@ use aws_sdk_kms::{ primitives::Blob, types::{KeySpec, MessageType, SigningAlgorithmSpec}, - // Client as AwsClient, }; use fuel_crypto::{Message, PublicKey, Signature}; use fuel_types::AssetId; @@ -21,8 +20,8 @@ use k256::{ PublicKey as K256PublicKey, }; -use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; use crate::aws::{AwsClient, AwsConfig}; +use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; @@ -36,7 +35,7 @@ pub struct AwsWallet { pub struct KmsData { id: String, client: AwsClient, - pub public_key: Vec, + public_key: Vec, pub address: Bech32Address, } @@ -74,7 +73,12 @@ impl KmsData { } async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { - let response = client.inner().get_public_key().key_id(key_id).send().await?; + let response = client + .inner() + .get_public_key() + .key_id(key_id) + .send() + .await?; let public_key = response .public_key() @@ -247,4 +251,4 @@ impl Account for AwsWallet { tb.add_signer(self.clone())?; Ok(()) } -} \ No newline at end of file +} diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 31b8b9402..0abc2c8a1 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -19,7 +19,6 @@ mod coin_cache; pub mod predicate; - #[cfg(test)] mod test { #[test] diff --git a/scripts/change-log/src/get_full_changelog.rs b/scripts/change-log/src/get_full_changelog.rs index a7c96a66a..3d096378e 100644 --- a/scripts/change-log/src/get_full_changelog.rs +++ b/scripts/change-log/src/get_full_changelog.rs @@ -53,7 +53,7 @@ pub async fn get_changelog_info( .as_ref() .map_or("misc", |title| title.split(':').next().unwrap_or("misc")) .to_string(); - let is_breaking = pr.title.as_ref().map_or(false, |title| title.contains('!')); + let is_breaking = pr.title.as_ref().is_some_and(|title| title.contains('!')); let title_description = pr .title From 7f5939d281815ad5f2e97e675e30a292c3442b84 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Wed, 12 Feb 2025 18:01:37 +0100 Subject: [PATCH 05/34] refactor --- e2e/src/e2e_helpers.rs | 6 +- e2e/src/kms.rs | 19 +- e2e/src/lib.rs | 10 +- packages/fuels-accounts/src/aws.rs | 5 - packages/fuels-accounts/src/kms.rs | 2 + packages/fuels-accounts/src/kms/aws.rs | 5 + .../{aws/aws_client.rs => kms/aws/client.rs} | 37 ++-- .../{aws/aws_signer.rs => kms/aws/wallet.rs} | 162 +++++++++--------- packages/fuels-accounts/src/lib.rs | 3 +- 9 files changed, 126 insertions(+), 123 deletions(-) delete mode 100644 packages/fuels-accounts/src/aws.rs create mode 100644 packages/fuels-accounts/src/kms.rs create mode 100644 packages/fuels-accounts/src/kms/aws.rs rename packages/fuels-accounts/src/{aws/aws_client.rs => kms/aws/client.rs} (51%) rename packages/fuels-accounts/src/{aws/aws_signer.rs => kms/aws/wallet.rs} (54%) diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs index c5cda7005..4571438a7 100644 --- a/e2e/src/e2e_helpers.rs +++ b/e2e/src/e2e_helpers.rs @@ -1,6 +1,6 @@ use crate::{ fuel_node::{FuelNode, FuelNodeProcess}, - kms::{Kms, KmsKey, KmsProcess}, + kms::{Kms, KmsProcess, KmsTestKey}, }; pub async fn start_kms(logs: bool) -> anyhow::Result { @@ -9,10 +9,10 @@ pub async fn start_kms(logs: bool) -> anyhow::Result { pub async fn create_and_fund_kms_keys( kms: &KmsProcess, fuel_node: &FuelNodeProcess, -) -> anyhow::Result { +) -> anyhow::Result { let amount = 5_000_000_000; let key = kms.create_key().await?; - let address = key.kms_data.address.clone(); + let address = key.kms_key.address().clone(); fuel_node.fund(address, amount).await?; Ok(key) diff --git a/e2e/src/kms.rs b/e2e/src/kms.rs index 70dd86762..7e350422a 100644 --- a/e2e/src/kms.rs +++ b/e2e/src/kms.rs @@ -1,5 +1,6 @@ use anyhow::Context; -use fuels::accounts::aws::{AwsClient, AwsConfig, KmsData}; +use fuels::accounts::kms::{AwsClient, AwsConfig}; +use fuels_accounts::kms::KmsKey; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; @@ -47,8 +48,8 @@ impl Kms { let port = container.get_host_port_ipv4(4566).await?; let url = format!("http://localhost:{}", port); - let config = AwsConfig::for_testing(url.clone()).await; - let client = AwsClient::new(config); + let config = AwsConfig::for_local_testing(url.clone()).await; + let client = AwsClient::new(&config); Ok(KmsProcess { _container: container, @@ -104,7 +105,7 @@ pub struct KmsProcess { } impl KmsProcess { - pub async fn create_key(&self) -> anyhow::Result { + pub async fn create_key(&self) -> anyhow::Result { let response = self .client .inner() @@ -120,19 +121,19 @@ impl KmsProcess { .and_then(|metadata| metadata.arn) .ok_or_else(|| anyhow::anyhow!("key arn missing from response"))?; - let kms_data = KmsData::new(id.clone(), self.client.clone()).await?; + let kms_key = KmsKey::new(id.clone(), self.client.clone()).await?; - Ok(KmsKey { + Ok(KmsTestKey { id, - kms_data, + kms_key, url: self.url.clone(), }) } } #[derive(Debug, Clone)] -pub struct KmsKey { +pub struct KmsTestKey { pub id: String, - pub kms_data: KmsData, + pub kms_key: KmsKey, pub url: String, } diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 2d0eaff00..b57b9f168 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -12,11 +12,11 @@ mod tests { use crate::e2e_helpers::{create_and_fund_kms_keys, start_fuel_node, start_kms}; use anyhow::Result; use fuels::prelude::{AssetId, Provider}; - use fuels_accounts::aws::AwsWallet; + use fuels_accounts::kms::AwsWallet; use fuels_accounts::ViewOnlyAccount; use std::str::FromStr; - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn fund_aws_wallet() -> Result<()> { let kms = start_kms(false).await?; let fuel_node = start_fuel_node(false).await?; @@ -32,7 +32,7 @@ mod tests { .expect("AssetId to be well formed"); let provider = Provider::connect(fuel_node.url()).await?; - let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; + let wallet = AwsWallet::with_kms_key(kms_key.id, Some(provider)).await?; let founded_coins = wallet .get_coins(asset_id) @@ -44,7 +44,7 @@ mod tests { Ok(()) } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn deploy_contract() -> anyhow::Result<()> { use fuels::prelude::*; @@ -62,7 +62,7 @@ mod tests { .expect("AssetId to be well formed"); let provider = Provider::connect(fuel_node.url()).await?; - let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; + let wallet = AwsWallet::with_kms_key(kms_key.id, Some(provider)).await?; let founded_coins = wallet .get_coins(asset_id) diff --git a/packages/fuels-accounts/src/aws.rs b/packages/fuels-accounts/src/aws.rs deleted file mode 100644 index 71491823d..000000000 --- a/packages/fuels-accounts/src/aws.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod aws_client; -mod aws_signer; - -pub use aws_client::*; -pub use aws_signer::*; diff --git a/packages/fuels-accounts/src/kms.rs b/packages/fuels-accounts/src/kms.rs new file mode 100644 index 000000000..d674313ec --- /dev/null +++ b/packages/fuels-accounts/src/kms.rs @@ -0,0 +1,2 @@ +mod aws; +pub use aws::*; diff --git a/packages/fuels-accounts/src/kms/aws.rs b/packages/fuels-accounts/src/kms/aws.rs new file mode 100644 index 000000000..f0b281aa9 --- /dev/null +++ b/packages/fuels-accounts/src/kms/aws.rs @@ -0,0 +1,5 @@ +mod client; +mod wallet; + +pub use client::*; +pub use wallet::*; diff --git a/packages/fuels-accounts/src/aws/aws_client.rs b/packages/fuels-accounts/src/kms/aws/client.rs similarity index 51% rename from packages/fuels-accounts/src/aws/aws_client.rs rename to packages/fuels-accounts/src/kms/aws/client.rs index 189b78dac..dc695585c 100644 --- a/packages/fuels-accounts/src/aws/aws_client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -1,7 +1,6 @@ -use aws_config::{default_provider::credentials::DefaultCredentialsChain, Region, SdkConfig}; -#[cfg(feature = "test-helpers")] -use aws_sdk_kms::config::Credentials; -use aws_sdk_kms::{config::BehaviorVersion, Client}; +use aws_config::{ + default_provider::credentials::DefaultCredentialsChain, BehaviorVersion, Region, SdkConfig, +}; #[derive(Debug, Clone)] pub struct AwsConfig { @@ -9,7 +8,7 @@ pub struct AwsConfig { } impl AwsConfig { - pub async fn from_env() -> Self { + pub async fn from_environment() -> Self { let loader = aws_config::defaults(BehaviorVersion::latest()) .credentials_provider(DefaultCredentialsChain::builder().build().await); @@ -19,24 +18,24 @@ impl AwsConfig { } #[cfg(feature = "test-helpers")] - pub async fn for_testing(url: String) -> Self { + pub async fn for_local_testing(endpoint_url: String) -> Self { let sdk_config = aws_config::defaults(BehaviorVersion::latest()) - .credentials_provider(Credentials::new( + .credentials_provider(aws_sdk_kms::config::Credentials::new( "test", "test", None, None, - "Static Credentials", + "Static Test Credentials", )) - .endpoint_url(url) - .region(Region::new("us-east-1")) // placeholder region for test + .endpoint_url(endpoint_url) + .region(Region::new("us-east-1")) // Test region .load() .await; Self { sdk_config } } - pub fn url(&self) -> Option<&str> { + pub fn endpoint_url(&self) -> Option<&str> { self.sdk_config.endpoint_url() } @@ -47,17 +46,17 @@ impl AwsConfig { #[derive(Clone, Debug)] pub struct AwsClient { - client: Client, + inner: aws_sdk_kms::Client, } impl AwsClient { - pub fn new(config: AwsConfig) -> Self { - let config = config.sdk_config; - let client = Client::new(&config); - - Self { client } + pub fn new(config: &AwsConfig) -> Self { + Self { + inner: aws_sdk_kms::Client::new(&config.sdk_config), + } } - pub fn inner(&self) -> &Client { - &self.client + + pub fn inner(&self) -> &aws_sdk_kms::Client { + &self.inner } } diff --git a/packages/fuels-accounts/src/aws/aws_signer.rs b/packages/fuels-accounts/src/kms/aws/wallet.rs similarity index 54% rename from packages/fuels-accounts/src/aws/aws_signer.rs rename to packages/fuels-accounts/src/kms/aws/wallet.rs index 462d318f3..fb76489ff 100644 --- a/packages/fuels-accounts/src/aws/aws_signer.rs +++ b/packages/fuels-accounts/src/kms/aws/wallet.rs @@ -1,3 +1,7 @@ +use crate::kms::aws::client::{AwsClient, AwsConfig}; +use crate::provider::Provider; +use crate::wallet::Wallet; +use crate::{Account, ViewOnlyAccount}; use aws_sdk_kms::{ primitives::Blob, types::{KeySpec, MessageType, SigningAlgorithmSpec}, @@ -20,123 +24,123 @@ use k256::{ PublicKey as K256PublicKey, }; -use crate::aws::{AwsClient, AwsConfig}; -use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; - const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; #[derive(Clone, Debug)] pub struct AwsWallet { - wallet: Wallet, - kms_data: KmsData, + view_account: Wallet, + kms_key: KmsKey, } #[derive(Clone, Debug)] -pub struct KmsData { - id: String, +pub struct KmsKey { + key_id: String, client: AwsClient, public_key: Vec, - pub address: Bech32Address, + fuel_address: Bech32Address, } -impl KmsData { - pub async fn new(id: String, client: AwsClient) -> anyhow::Result { - Self::validate_key_type(&client, &id).await?; - let public_key = Self::fetch_public_key(&client, &id).await?; - let address = Self::create_bech32_address(&public_key)?; +impl KmsKey { + pub async fn new(key_id: String, client: AwsClient) -> Result { + Self::validate_key_type(&client, &key_id).await?; + let public_key = Self::fetch_public_key(&client, &key_id).await?; + let fuel_address = Self::derive_fuel_address(&public_key)?; Ok(Self { - id, + key_id, client, public_key, - address, + fuel_address, }) } - async fn validate_key_type(client: &AwsClient, key_id: &str) -> anyhow::Result<()> { + async fn validate_key_type(client: &AwsClient, key_id: &str) -> Result<()> { let key_spec = client .inner() .get_public_key() .key_id(key_id) .send() - .await? + .await + .map_err(|e| Error::Other(format!("{}: {}", AWS_KMS_ERROR_PREFIX, e)))? .key_spec; match key_spec { Some(KeySpec::EccSecgP256K1) => Ok(()), - other => anyhow::bail!( - "{}: Invalid key type, expected EccSecgP256K1, got {:?}", - AWS_KMS_ERROR_PREFIX, - other - ), + other => Err(Error::Other(format!( + "{}: Invalid key type {:?}, expected ECC_SECG_P256K1", + AWS_KMS_ERROR_PREFIX, other + ))), } } - async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { + async fn fetch_public_key(client: &AwsClient, key_id: &str) -> Result> { let response = client .inner() .get_public_key() .key_id(key_id) .send() - .await?; + .await + .map_err(|e| Error::Other(format!("{}: {}", AWS_KMS_ERROR_PREFIX, e)))?; - let public_key = response + response .public_key() - .ok_or_else(|| anyhow::anyhow!("{}: No public key returned", AWS_KMS_ERROR_PREFIX))?; - - Ok(public_key.clone().into_inner()) + .map(|blob| blob.as_ref().to_vec()) + .ok_or_else(|| { + Error::Other(format!( + "{}: Empty public key response", + AWS_KMS_ERROR_PREFIX + )) + }) } - fn create_bech32_address(public_key_bytes: &[u8]) -> anyhow::Result { - let k256_public_key = K256PublicKey::from_public_key_der(public_key_bytes) - .map_err(|_| anyhow::anyhow!("{}: Invalid DER public key", AWS_KMS_ERROR_PREFIX))?; + fn derive_fuel_address(public_key: &[u8]) -> Result { + let k256_key = K256PublicKey::from_public_key_der(public_key) + .map_err(|_| Error::Other(format!("{}: Invalid DER encoding", AWS_KMS_ERROR_PREFIX)))?; - let public_key = PublicKey::from(k256_public_key); - let fuel_address = public_key.hash(); - - Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_address)) + let fuel_public_key = PublicKey::from(k256_key); + Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_public_key.hash())) } async fn sign_message(&self, message: Message) -> Result { - let signature_der = self.request_signature(message).await?; - let (signature, recovery_id) = self.process_signature(&signature_der, message)?; + let signature_der = self.request_kms_signature(message).await?; + let (sig, recovery_id) = self.normalize_signature(&signature_der, message)?; - Ok(self.create_fuel_signature(signature, recovery_id)) + Ok(self.format_fuel_signature(sig, recovery_id)) } - async fn request_signature(&self, message: Message) -> Result> { - let reply = self - .client + async fn request_kms_signature(&self, message: Message) -> Result> { + self.client .inner() .sign() - .key_id(&self.id) + .key_id(&self.key_id) .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) .message_type(MessageType::Digest) - .message(Blob::new(*message)) + .message(Blob::new(message.as_ref().to_vec())) .send() .await - .map_err(|e| { - Error::Other(format!("{}: Failed to sign: {:?}", AWS_KMS_ERROR_PREFIX, e)) - })?; - - reply + .map_err(|e| Error::Other(format!("{}: Signing failed - {}", AWS_KMS_ERROR_PREFIX, e)))? .signature - .map(|sig| sig.into_inner()) - .ok_or_else(|| Error::Other(format!("{}: No signature returned", AWS_KMS_ERROR_PREFIX))) + .map(|blob| blob.into_inner()) + .ok_or_else(|| { + Error::Other(format!( + "{}: Empty signature response", + AWS_KMS_ERROR_PREFIX + )) + }) } - fn process_signature( + fn normalize_signature( &self, signature_der: &[u8], message: Message, ) -> Result<(K256Signature, RecoveryId)> { - let sig = K256Signature::from_der(signature_der).map_err(|_| { + let mut sig = K256Signature::from_der(signature_der).map_err(|_| { Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) })?; - let sig = sig.normalize_s().unwrap_or(sig); - let recovery_id = self.determine_recovery_id(&sig, message)?; + sig = sig.normalize_s().unwrap_or(sig); + let recovery_id = self.determine_recovery_id(&sig, message)?; Ok((sig, recovery_id)) } @@ -168,68 +172,66 @@ impl KmsData { } } - fn create_fuel_signature( + fn format_fuel_signature( &self, signature: K256Signature, recovery_id: RecoveryId, ) -> Signature { - debug_assert!( - !recovery_id.is_x_reduced(), - "reduced-x form coordinates should be caught earlier" - ); - - let v = recovery_id.is_y_odd() as u8; - let mut signature_bytes = <[u8; 64]>::from(signature.to_bytes()); - signature_bytes[32] = (v << 7) | (signature_bytes[32] & 0x7f); + let recovery_byte = recovery_id.is_y_odd() as u8; + let mut bytes: [u8; 64] = signature.to_bytes().into(); + bytes[63] = (recovery_byte << 7) | (bytes[63] & 0x7F); + Signature::from_bytes(bytes) + } - Signature::from_bytes(signature_bytes) + pub fn address(&self) -> &Bech32Address { + &self.fuel_address } } impl AwsWallet { - pub async fn from_kms_key_id( - kms_key_id: String, + pub async fn with_kms_key( + key_id: impl Into, provider: Option, - ) -> anyhow::Result { - let config = AwsConfig::from_env().await; - let client = AwsClient::new(config); - let kms_data = KmsData::new(kms_key_id, client).await?; + ) -> Result { + let config = AwsConfig::from_environment().await; + let client = AwsClient::new(&config); + let kms_key = KmsKey::new(key_id.into(), client).await?; Ok(Self { - wallet: Wallet::from_address(kms_data.address.clone(), provider), - kms_data, + view_account: Wallet::from_address(kms_key.fuel_address.clone(), provider), + kms_key, }) } pub fn address(&self) -> &Bech32Address { - self.wallet.address() + &self.kms_key.fuel_address } pub fn provider(&self) -> Option<&Provider> { - self.wallet.provider() + self.view_account.provider() } } #[async_trait::async_trait] impl Signer for AwsWallet { async fn sign(&self, message: Message) -> Result { - self.kms_data.sign_message(message).await + self.kms_key.sign_message(message).await } fn address(&self) -> &Bech32Address { - &self.kms_data.address + self.address() } } #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl ViewOnlyAccount for AwsWallet { fn address(&self) -> &Bech32Address { - self.wallet.address() + &self.kms_key.fuel_address } fn try_provider(&self) -> Result<&Provider> { - self.wallet.provider().ok_or_else(|| { - Error::Other("No provider available. Make sure to use `set_provider`".to_owned()) + self.provider().ok_or_else(|| { + Error::Other("Provider required - use `.with_provider()` when creating wallet".into()) }) } @@ -239,7 +241,7 @@ impl ViewOnlyAccount for AwsWallet { amount: u64, excluded_coins: Option>, ) -> Result> { - self.wallet + self.view_account .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) .await } diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 0abc2c8a1..ca1155f9f 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -12,13 +12,12 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; #[cfg(feature = "std")] -pub mod aws; +pub mod kms; #[cfg(feature = "coin-cache")] mod coin_cache; pub mod predicate; - #[cfg(test)] mod test { #[test] From 1a83c2fb81dd81475e7e2bf1e44e6d7f528e1f98 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 13 Feb 2025 16:51:05 +0100 Subject: [PATCH 06/34] addd google kms client --- e2e/Cargo.toml | 9 +- e2e/src/{kms.rs => aws_kms.rs} | 22 +- e2e/src/e2e_helpers.rs | 28 +- e2e/src/google_kms.rs | 247 ++++++++++++++++++ e2e/src/lib.rs | 30 ++- packages/fuels-accounts/Cargo.toml | 7 +- packages/fuels-accounts/src/kms.rs | 2 + packages/fuels-accounts/src/kms/google.rs | 1 + .../fuels-accounts/src/kms/google/client.rs | 44 ++++ 9 files changed, 366 insertions(+), 24 deletions(-) rename e2e/src/{kms.rs => aws_kms.rs} (91%) create mode 100644 e2e/src/google_kms.rs create mode 100644 packages/fuels-accounts/src/kms/google.rs create mode 100644 packages/fuels-accounts/src/kms/google/client.rs diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 1013d1415..6ce35105e 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -46,6 +46,9 @@ aws-sdk-kms = { workspace = true, features = ["rustls"] } fuel-core-client = { workspace = true } +google-cloud-kms = { version = "0.6.0"} +tonic = { version = "0.12.3", features = ["tls"] } +tonic-types = { version = "0.12.3" } [build-dependencies] @@ -64,4 +67,8 @@ rocksdb = ["fuels/rocksdb"] coin-cache = ["fuels/coin-cache"] [dependencies] anyhow = "1.0.86" -url = "2.5.2" \ No newline at end of file +url = "2.5.2" +serde_json = "1.0.120" +reqwest = "0.12.5" +tokio-stream = "0.1.17" +async-stream = "0.3.5" \ No newline at end of file diff --git a/e2e/src/kms.rs b/e2e/src/aws_kms.rs similarity index 91% rename from e2e/src/kms.rs rename to e2e/src/aws_kms.rs index 7e350422a..6f411167e 100644 --- a/e2e/src/kms.rs +++ b/e2e/src/aws_kms.rs @@ -5,13 +5,13 @@ use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; #[derive(Default)] -pub struct Kms { +pub struct AwsKms { show_logs: bool, } -struct KmsImage; +struct AwsKmsImage; -impl testcontainers::Image for KmsImage { +impl testcontainers::Image for AwsKmsImage { fn name(&self) -> &str { "localstack/localstack" } @@ -29,14 +29,14 @@ impl testcontainers::Image for KmsImage { } } -impl Kms { +impl AwsKms { pub fn with_show_logs(mut self, show_logs: bool) -> Self { self.show_logs = show_logs; self } - pub async fn start(self) -> anyhow::Result { - let container = KmsImage + pub async fn start(self) -> anyhow::Result { + let container = AwsKmsImage .start() .await .with_context(|| "Failed to start KMS container")?; @@ -51,7 +51,7 @@ impl Kms { let config = AwsConfig::for_local_testing(url.clone()).await; let client = AwsClient::new(&config); - Ok(KmsProcess { + Ok(AwsKmsProcess { _container: container, client, url, @@ -59,7 +59,7 @@ impl Kms { } } -fn spawn_log_printer(container: &testcontainers::ContainerAsync) { +fn spawn_log_printer(container: &testcontainers::ContainerAsync) { let stderr = container.stderr(true); let stdout = container.stdout(true); tokio::spawn(async move { @@ -98,13 +98,13 @@ fn spawn_log_printer(container: &testcontainers::ContainerAsync) { }); } -pub struct KmsProcess { - _container: testcontainers::ContainerAsync, +pub struct AwsKmsProcess { + _container: testcontainers::ContainerAsync, client: AwsClient, url: String, } -impl KmsProcess { +impl AwsKmsProcess { pub async fn create_key(&self) -> anyhow::Result { let response = self .client diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs index 4571438a7..e2eaae3ed 100644 --- a/e2e/src/e2e_helpers.rs +++ b/e2e/src/e2e_helpers.rs @@ -1,13 +1,19 @@ +use crate::google_kms::{GoogleKms, GoogleKmsProcess}; use crate::{ + aws_kms::{AwsKms, AwsKmsProcess, KmsTestKey}, fuel_node::{FuelNode, FuelNodeProcess}, - kms::{Kms, KmsProcess, KmsTestKey}, }; -pub async fn start_kms(logs: bool) -> anyhow::Result { - Kms::default().with_show_logs(logs).start().await +pub async fn start_aws_kms(logs: bool) -> anyhow::Result { + AwsKms::default().with_show_logs(logs).start().await } -pub async fn create_and_fund_kms_keys( - kms: &KmsProcess, + +pub async fn start_google_kms(logs: bool) -> anyhow::Result { + GoogleKms::default().with_show_logs(logs).start().await +} + +pub async fn create_and_fund_aws_kms_key( + kms: &AwsKmsProcess, fuel_node: &FuelNodeProcess, ) -> anyhow::Result { let amount = 5_000_000_000; @@ -20,3 +26,15 @@ pub async fn create_and_fund_kms_keys( pub async fn start_fuel_node(logs: bool) -> anyhow::Result { FuelNode::default().with_show_logs(logs).start().await } + +pub async fn create_and_fund_google_kms_key( + kms: &GoogleKmsProcess, + fuel_node: &FuelNodeProcess, +) -> anyhow::Result<()> { + // let amount = 5_000_000_000; + // let key = kms.create_key().await?; + // let address = key.kms_key.address().clone(); + // fuel_node.fund(address, amount).await?; + + Ok(()) +} diff --git a/e2e/src/google_kms.rs b/e2e/src/google_kms.rs new file mode 100644 index 000000000..17084e3d4 --- /dev/null +++ b/e2e/src/google_kms.rs @@ -0,0 +1,247 @@ +use anyhow::Context; +use google_cloud_kms::grpc::kms::v1::crypto_key::CryptoKeyPurpose; +use google_cloud_kms::grpc::kms::v1::key_management_service_client::KeyManagementServiceClient; +use google_cloud_kms::grpc::kms::v1::CreateKeyRingRequest; +use google_cloud_kms::grpc::kms::v1::CryptoKey; +use google_cloud_kms::grpc::kms::v1::CryptoKeyVersionTemplate; +use google_cloud_kms::grpc::kms::v1::KeyRing; +use google_cloud_kms::grpc::kms::v1::ProtectionLevel; +use google_cloud_kms::grpc::kms::v1::{crypto_key_version, CreateCryptoKeyRequest}; +use reqwest; +use serde_json::json; +use testcontainers::{core::ContainerPort, runners::AsyncRunner}; +use tokio::io::AsyncBufReadExt; +use tonic::transport::Channel; + +struct MockServerImage; + +impl testcontainers::Image for MockServerImage { + fn name(&self) -> &str { + "mockserver/mockserver" + } + + fn tag(&self) -> &str { + "latest" + } + + fn ready_conditions(&self) -> Vec { + vec![testcontainers::core::WaitFor::message_on_stdout( + "started on port", + )] + } + + fn expose_ports(&self) -> &[ContainerPort] { + &[ContainerPort::Tcp(1080)] // MockServer default port + } +} + +#[derive(Default)] +pub struct GoogleKms { + show_logs: bool, +} + +impl GoogleKms { + pub fn with_show_logs(mut self, show_logs: bool) -> Self { + self.show_logs = show_logs; + self + } + + pub async fn start(self) -> anyhow::Result { + let container = MockServerImage + .start() + .await + .with_context(|| "Failed to start MockServer container")?; + + if self.show_logs { + spawn_log_printer(&container); + } + + let port = container.get_host_port_ipv4(1080).await?; + let mock_endpoint = format!("http://localhost:{}", port); + + // Configure mock responses + Self::setup_mock_responses(&mock_endpoint).await?; + + // Create gRPC client with the mock endpoint + let channel = Channel::from_shared(mock_endpoint.clone()) + .context("Invalid endpoint URL")? + .connect() + .await + .context("Failed to connect to mock KMS")?; + + let client = KeyManagementServiceClient::new(channel); + + Ok(GoogleKmsProcess { + _container: container, + client, + endpoint: mock_endpoint, + }) + } + + async fn setup_mock_responses(endpoint: &str) -> anyhow::Result<()> { + let client = reqwest::Client::new(); + + // Mock response for CreateKeyRing + let create_keyring_expectation = json!({ + "httpRequest": { + "method": "POST", + "path": "/v1/projects/test-project/locations/global/keyRings.*" + }, + "httpResponse": { + "statusCode": 200, + "headers": { + "content-type": ["application/json"] + }, + "body": { + "name": "projects/test-project/locations/global/keyRings/fuel-test-keyring", + "createTime": "2024-02-13T10:00:00Z" + } + } + }); + + // Mock response for CreateCryptoKey + let create_key_expectation = json!({ + "httpRequest": { + "method": "POST", + "path": "/v1/projects/test-project/locations/global/keyRings/.*/cryptoKeys.*" + }, + "httpResponse": { + "statusCode": 200, + "headers": { + "content-type": ["application/json"] + }, + "body": { + "name": "projects/test-project/locations/global/keyRings/fuel-test-keyring/cryptoKeys/fuel-signing-key", + "primary": { + "name": "projects/test-project/locations/global/keyRings/fuel-test-keyring/cryptoKeys/fuel-signing-key/cryptoKeyVersions/1", + "state": "ENABLED", + "algorithm": "EC_SIGN_SECP256K1_SHA256", + "protectionLevel": "SOFTWARE" + } + } + } + }); + + // Set up expectations + client + .put(format!("{}/mockserver/expectation", endpoint).as_str()) + .json(&create_keyring_expectation) + .send() + .await?; + + client + .put(format!("{}/mockserver/expectation", endpoint).as_str()) + .json(&create_key_expectation) + .send() + .await?; + + Ok(()) + } +} + +fn spawn_log_printer(container: &testcontainers::ContainerAsync) { + let stderr = container.stderr(true); + let stdout = container.stdout(true); + tokio::spawn(async move { + let mut stderr_lines = stderr.lines(); + let mut stdout_lines = stdout.lines(); + + let mut other_stream_closed = false; + loop { + tokio::select! { + stderr_result = stderr_lines.next_line() => { + match stderr_result { + Ok(Some(line)) => eprintln!("MockServer (stderr): {}", line), + Ok(None) if other_stream_closed => break, + Ok(None) => other_stream_closed = true, + Err(e) => { + eprintln!("MockServer: Error reading from stderr: {:?}", e); + break; + } + } + } + stdout_result = stdout_lines.next_line() => { + match stdout_result { + Ok(Some(line)) => eprintln!("MockServer (stdout): {}", line), + Ok(None) if other_stream_closed => break, + Ok(None) => other_stream_closed = true, + Err(e) => { + eprintln!("MockServer: Error reading from stdout: {:?}", e); + break; + } + } + } + } + } + + Ok::<(), std::io::Error>(()) + }); +} + +pub struct GoogleKmsProcess { + _container: testcontainers::ContainerAsync, + client: KeyManagementServiceClient, + pub endpoint: String, +} + +impl GoogleKmsProcess { + pub async fn create_key(&mut self) -> anyhow::Result { + // Create key ring + let key_ring_id = "fuel-test-keyring"; + let parent = "projects/test-project/locations/global".to_string(); + let key_ring_path = format!("{}/keyRings/{}", parent, key_ring_id); + + let create_ring_result = self + .client + .create_key_ring(CreateKeyRingRequest { + parent, + key_ring_id: key_ring_id.to_string(), + key_ring: Some(KeyRing::default()), + }) + .await; + + if let Err(e) = &create_ring_result { + if !e.to_string().contains("already exists") { + create_ring_result.context("Failed to create key ring")?; + } + } + + // Create crypto key + let key_id = "fuel-signing-key"; + let key_path = format!("{}/cryptoKeys/{}", key_ring_path, key_id); + + let request = CreateCryptoKeyRequest { + parent: key_ring_path, + crypto_key_id: key_id.to_string(), + crypto_key: Some(CryptoKey { + purpose: CryptoKeyPurpose::AsymmetricSign as i32, + version_template: Some(CryptoKeyVersionTemplate { + algorithm: crypto_key_version::CryptoKeyVersionAlgorithm::EcSignP256Sha256 + as i32, + protection_level: ProtectionLevel::Software as i32, + ..Default::default() + }), + ..Default::default() + }), + skip_initial_version_creation: false, + }; + + let create_key_result = self.client.create_crypto_key(request).await; + + if let Err(e) = &create_key_result { + if !e.to_string().contains("already exists") { + create_key_result.context("Failed to create crypto key")?; + } + } + + Ok(KmsTestKey { + name: key_path, + endpoint: self.endpoint.clone(), + }) + } +} +#[derive(Debug, Clone)] +pub struct KmsTestKey { + pub name: String, + pub endpoint: String, +} diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index b57b9f168..77d69f6e7 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -1,26 +1,32 @@ #[cfg(test)] +mod aws_kms; +#[cfg(test)] mod client; #[cfg(test)] mod e2e_helpers; #[cfg(test)] mod fuel_node; #[cfg(test)] -mod kms; +mod google_kms; #[cfg(test)] mod tests { - use crate::e2e_helpers::{create_and_fund_kms_keys, start_fuel_node, start_kms}; + use crate::e2e_helpers::{ + create_and_fund_aws_kms_key, create_and_fund_google_kms_key, start_aws_kms, + start_fuel_node, start_google_kms, + }; use anyhow::Result; use fuels::prelude::{AssetId, Provider}; use fuels_accounts::kms::AwsWallet; use fuels_accounts::ViewOnlyAccount; + use google_cloud_kms::client::{Client, ClientConfig}; use std::str::FromStr; #[tokio::test(flavor = "multi_thread")] async fn fund_aws_wallet() -> Result<()> { - let kms = start_kms(false).await?; + let kms = start_aws_kms(false).await?; let fuel_node = start_fuel_node(false).await?; - let kms_key = create_and_fund_kms_keys(&kms, &fuel_node).await?; + let kms_key = create_and_fund_aws_kms_key(&kms, &fuel_node).await?; std::env::set_var("AWS_ACCESS_KEY_ID", "test"); std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); @@ -48,9 +54,9 @@ mod tests { async fn deploy_contract() -> anyhow::Result<()> { use fuels::prelude::*; - let kms = start_kms(false).await?; + let kms = start_aws_kms(false).await?; let fuel_node = start_fuel_node(false).await?; - let kms_key = create_and_fund_kms_keys(&kms, &fuel_node).await?; + let kms_key = create_and_fund_aws_kms_key(&kms, &fuel_node).await?; std::env::set_var("AWS_ACCESS_KEY_ID", "test"); std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); @@ -91,4 +97,16 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread")] + async fn fund_google_wallet() -> anyhow::Result<()> { + let mut kms = start_google_kms(true).await?; + let fuel_node = start_fuel_node(false).await?; + let a = kms.create_key().await?; + // dbg!(a.name); + // let kms_key = create_and_fund_google_kms_key(&kms, &fuel_node).await?; + // let a = kms.create_key().await?; + // dbg!(a.name); + Ok(()) + } } diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 8c74ae876..531f97432 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -28,14 +28,19 @@ tai64 = { workspace = true, features = ["serde"] } thiserror = { workspace = true, default-features = false } tokio = { workspace = true, features = ["full"], optional = true } zeroize = { workspace = true, features = ["derive"] } -anyhow = "1.0.86" k256 = { workspace = true, features = ["ecdsa-core"] } +# AWS KMS client aws-config = { workspace = true, features = [ "behavior-version-latest", ] } aws-sdk-kms = { workspace = true, features = ["default"] } +# Google Cloud KMS client +google-cloud-kms = "0.6.0" +google-cloud-auth = "0.17.2" +hyper-rustls = "0.27.5" + [dev-dependencies] mockall = { workspace = true, default-features = false } fuel-tx = { workspace = true, features = ["test-helpers", "random"] } diff --git a/packages/fuels-accounts/src/kms.rs b/packages/fuels-accounts/src/kms.rs index d674313ec..283a2db18 100644 --- a/packages/fuels-accounts/src/kms.rs +++ b/packages/fuels-accounts/src/kms.rs @@ -1,2 +1,4 @@ mod aws; +mod google; + pub use aws::*; diff --git a/packages/fuels-accounts/src/kms/google.rs b/packages/fuels-accounts/src/kms/google.rs new file mode 100644 index 000000000..b79c47fca --- /dev/null +++ b/packages/fuels-accounts/src/kms/google.rs @@ -0,0 +1 @@ +mod client; diff --git a/packages/fuels-accounts/src/kms/google/client.rs b/packages/fuels-accounts/src/kms/google/client.rs new file mode 100644 index 000000000..9ecc0518c --- /dev/null +++ b/packages/fuels-accounts/src/kms/google/client.rs @@ -0,0 +1,44 @@ +use fuels_core::types::errors::{Error, Result}; +use google_cloud_auth::credentials::CredentialsFile; +use google_cloud_kms::client::{Client, ClientConfig}; +use std::env; +#[derive(Debug)] +pub struct GcpKmsConfig { + client_config: ClientConfig, +} + +impl GcpKmsConfig { + pub async fn from_environment() -> Result { + let credentials_path = env::var("GOOGLE_APPLICATION_CREDENTIALS") + .map_err(|_| Error::Other("GOOGLE_APPLICATION_CREDENTIALS not set".to_string()))?; + + let credentials = CredentialsFile::new_from_file(credentials_path) + .await + .map_err(|e| Error::Other(format!("Failed to load Google KMS credentials: {}", e)))?; + + let client_config = ClientConfig::default() + .with_credentials(credentials) + .await + .map_err(|e| Error::Other(format!("Failed to create Google KMS client: {}", e)))?; + Ok(Self { client_config }) + } +} + +#[derive(Clone, Debug)] +pub struct GcpKmsClient { + inner: Client, +} + +impl GcpKmsClient { + pub async fn new(config: GcpKmsConfig) -> Result { + let client = Client::new(config.client_config) + .await + .map_err(|e| Error::Other(format!("Failed to create Google KMS client: {}", e)))?; + + Ok(Self { inner: client }) + } + + pub fn inner(&self) -> &Client { + &self.inner + } +} From ec7b0125f38a5ebf5efd44b70be7b2400d22b262 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 12:43:15 +0100 Subject: [PATCH 07/34] update --- docs/src/SUMMARY.md | 1 + docs/src/wallets/kms-wallets.md | 7 + e2e/src/google_kms.rs | 247 ------------------ e2e/src/lib.rs | 13 +- packages/fuels-accounts/src/kms/google.rs | 1 - .../fuels-accounts/src/kms/google/client.rs | 44 ---- 6 files changed, 9 insertions(+), 304 deletions(-) create mode 100644 docs/src/wallets/kms-wallets.md delete mode 100644 e2e/src/google_kms.rs delete mode 100644 packages/fuels-accounts/src/kms/google.rs delete mode 100644 packages/fuels-accounts/src/kms/google/client.rs diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a6f264661..cc56a646c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -13,6 +13,7 @@ - [Managing wallets](./wallets/index.md) - [Creating a wallet from a private key](./wallets/private-keys.md) - [Creating a wallet from mnemonic phrases](./wallets/mnemonic-wallet.md) + - [KMS wallets](./wallets/kms-wallets.md) - [Wallet Access](./wallets/access.md) - [Encrypting and storing wallets](./wallets/encrypting-and-storing.md) - [Checking balances and coins](./wallets/checking-balances-and-coins.md) diff --git a/docs/src/wallets/kms-wallets.md b/docs/src/wallets/kms-wallets.md new file mode 100644 index 000000000..b335b6ac0 --- /dev/null +++ b/docs/src/wallets/kms-wallets.md @@ -0,0 +1,7 @@ +# Using AWS KMS Wallets + +AWS Key Management Service (KMS) provides a secure way to manage cryptographic keys for your Fuel wallets. Rather than storing private keys locally, AWS KMS wallets use AWS's secure infrastructure to handle key storage and signing operations. + +```rust,ignore +{{#include ../../../e2e/src/lib.rs:use_kms_wallet}} +``` \ No newline at end of file diff --git a/e2e/src/google_kms.rs b/e2e/src/google_kms.rs deleted file mode 100644 index 17084e3d4..000000000 --- a/e2e/src/google_kms.rs +++ /dev/null @@ -1,247 +0,0 @@ -use anyhow::Context; -use google_cloud_kms::grpc::kms::v1::crypto_key::CryptoKeyPurpose; -use google_cloud_kms::grpc::kms::v1::key_management_service_client::KeyManagementServiceClient; -use google_cloud_kms::grpc::kms::v1::CreateKeyRingRequest; -use google_cloud_kms::grpc::kms::v1::CryptoKey; -use google_cloud_kms::grpc::kms::v1::CryptoKeyVersionTemplate; -use google_cloud_kms::grpc::kms::v1::KeyRing; -use google_cloud_kms::grpc::kms::v1::ProtectionLevel; -use google_cloud_kms::grpc::kms::v1::{crypto_key_version, CreateCryptoKeyRequest}; -use reqwest; -use serde_json::json; -use testcontainers::{core::ContainerPort, runners::AsyncRunner}; -use tokio::io::AsyncBufReadExt; -use tonic::transport::Channel; - -struct MockServerImage; - -impl testcontainers::Image for MockServerImage { - fn name(&self) -> &str { - "mockserver/mockserver" - } - - fn tag(&self) -> &str { - "latest" - } - - fn ready_conditions(&self) -> Vec { - vec![testcontainers::core::WaitFor::message_on_stdout( - "started on port", - )] - } - - fn expose_ports(&self) -> &[ContainerPort] { - &[ContainerPort::Tcp(1080)] // MockServer default port - } -} - -#[derive(Default)] -pub struct GoogleKms { - show_logs: bool, -} - -impl GoogleKms { - pub fn with_show_logs(mut self, show_logs: bool) -> Self { - self.show_logs = show_logs; - self - } - - pub async fn start(self) -> anyhow::Result { - let container = MockServerImage - .start() - .await - .with_context(|| "Failed to start MockServer container")?; - - if self.show_logs { - spawn_log_printer(&container); - } - - let port = container.get_host_port_ipv4(1080).await?; - let mock_endpoint = format!("http://localhost:{}", port); - - // Configure mock responses - Self::setup_mock_responses(&mock_endpoint).await?; - - // Create gRPC client with the mock endpoint - let channel = Channel::from_shared(mock_endpoint.clone()) - .context("Invalid endpoint URL")? - .connect() - .await - .context("Failed to connect to mock KMS")?; - - let client = KeyManagementServiceClient::new(channel); - - Ok(GoogleKmsProcess { - _container: container, - client, - endpoint: mock_endpoint, - }) - } - - async fn setup_mock_responses(endpoint: &str) -> anyhow::Result<()> { - let client = reqwest::Client::new(); - - // Mock response for CreateKeyRing - let create_keyring_expectation = json!({ - "httpRequest": { - "method": "POST", - "path": "/v1/projects/test-project/locations/global/keyRings.*" - }, - "httpResponse": { - "statusCode": 200, - "headers": { - "content-type": ["application/json"] - }, - "body": { - "name": "projects/test-project/locations/global/keyRings/fuel-test-keyring", - "createTime": "2024-02-13T10:00:00Z" - } - } - }); - - // Mock response for CreateCryptoKey - let create_key_expectation = json!({ - "httpRequest": { - "method": "POST", - "path": "/v1/projects/test-project/locations/global/keyRings/.*/cryptoKeys.*" - }, - "httpResponse": { - "statusCode": 200, - "headers": { - "content-type": ["application/json"] - }, - "body": { - "name": "projects/test-project/locations/global/keyRings/fuel-test-keyring/cryptoKeys/fuel-signing-key", - "primary": { - "name": "projects/test-project/locations/global/keyRings/fuel-test-keyring/cryptoKeys/fuel-signing-key/cryptoKeyVersions/1", - "state": "ENABLED", - "algorithm": "EC_SIGN_SECP256K1_SHA256", - "protectionLevel": "SOFTWARE" - } - } - } - }); - - // Set up expectations - client - .put(format!("{}/mockserver/expectation", endpoint).as_str()) - .json(&create_keyring_expectation) - .send() - .await?; - - client - .put(format!("{}/mockserver/expectation", endpoint).as_str()) - .json(&create_key_expectation) - .send() - .await?; - - Ok(()) - } -} - -fn spawn_log_printer(container: &testcontainers::ContainerAsync) { - let stderr = container.stderr(true); - let stdout = container.stdout(true); - tokio::spawn(async move { - let mut stderr_lines = stderr.lines(); - let mut stdout_lines = stdout.lines(); - - let mut other_stream_closed = false; - loop { - tokio::select! { - stderr_result = stderr_lines.next_line() => { - match stderr_result { - Ok(Some(line)) => eprintln!("MockServer (stderr): {}", line), - Ok(None) if other_stream_closed => break, - Ok(None) => other_stream_closed = true, - Err(e) => { - eprintln!("MockServer: Error reading from stderr: {:?}", e); - break; - } - } - } - stdout_result = stdout_lines.next_line() => { - match stdout_result { - Ok(Some(line)) => eprintln!("MockServer (stdout): {}", line), - Ok(None) if other_stream_closed => break, - Ok(None) => other_stream_closed = true, - Err(e) => { - eprintln!("MockServer: Error reading from stdout: {:?}", e); - break; - } - } - } - } - } - - Ok::<(), std::io::Error>(()) - }); -} - -pub struct GoogleKmsProcess { - _container: testcontainers::ContainerAsync, - client: KeyManagementServiceClient, - pub endpoint: String, -} - -impl GoogleKmsProcess { - pub async fn create_key(&mut self) -> anyhow::Result { - // Create key ring - let key_ring_id = "fuel-test-keyring"; - let parent = "projects/test-project/locations/global".to_string(); - let key_ring_path = format!("{}/keyRings/{}", parent, key_ring_id); - - let create_ring_result = self - .client - .create_key_ring(CreateKeyRingRequest { - parent, - key_ring_id: key_ring_id.to_string(), - key_ring: Some(KeyRing::default()), - }) - .await; - - if let Err(e) = &create_ring_result { - if !e.to_string().contains("already exists") { - create_ring_result.context("Failed to create key ring")?; - } - } - - // Create crypto key - let key_id = "fuel-signing-key"; - let key_path = format!("{}/cryptoKeys/{}", key_ring_path, key_id); - - let request = CreateCryptoKeyRequest { - parent: key_ring_path, - crypto_key_id: key_id.to_string(), - crypto_key: Some(CryptoKey { - purpose: CryptoKeyPurpose::AsymmetricSign as i32, - version_template: Some(CryptoKeyVersionTemplate { - algorithm: crypto_key_version::CryptoKeyVersionAlgorithm::EcSignP256Sha256 - as i32, - protection_level: ProtectionLevel::Software as i32, - ..Default::default() - }), - ..Default::default() - }), - skip_initial_version_creation: false, - }; - - let create_key_result = self.client.create_crypto_key(request).await; - - if let Err(e) = &create_key_result { - if !e.to_string().contains("already exists") { - create_key_result.context("Failed to create crypto key")?; - } - } - - Ok(KmsTestKey { - name: key_path, - endpoint: self.endpoint.clone(), - }) - } -} -#[derive(Debug, Clone)] -pub struct KmsTestKey { - pub name: String, - pub endpoint: String, -} diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 77d69f6e7..18ed8c4e8 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -97,16 +97,5 @@ mod tests { Ok(()) } - - #[tokio::test(flavor = "multi_thread")] - async fn fund_google_wallet() -> anyhow::Result<()> { - let mut kms = start_google_kms(true).await?; - let fuel_node = start_fuel_node(false).await?; - let a = kms.create_key().await?; - // dbg!(a.name); - // let kms_key = create_and_fund_google_kms_key(&kms, &fuel_node).await?; - // let a = kms.create_key().await?; - // dbg!(a.name); - Ok(()) - } + } diff --git a/packages/fuels-accounts/src/kms/google.rs b/packages/fuels-accounts/src/kms/google.rs deleted file mode 100644 index b79c47fca..000000000 --- a/packages/fuels-accounts/src/kms/google.rs +++ /dev/null @@ -1 +0,0 @@ -mod client; diff --git a/packages/fuels-accounts/src/kms/google/client.rs b/packages/fuels-accounts/src/kms/google/client.rs deleted file mode 100644 index 9ecc0518c..000000000 --- a/packages/fuels-accounts/src/kms/google/client.rs +++ /dev/null @@ -1,44 +0,0 @@ -use fuels_core::types::errors::{Error, Result}; -use google_cloud_auth::credentials::CredentialsFile; -use google_cloud_kms::client::{Client, ClientConfig}; -use std::env; -#[derive(Debug)] -pub struct GcpKmsConfig { - client_config: ClientConfig, -} - -impl GcpKmsConfig { - pub async fn from_environment() -> Result { - let credentials_path = env::var("GOOGLE_APPLICATION_CREDENTIALS") - .map_err(|_| Error::Other("GOOGLE_APPLICATION_CREDENTIALS not set".to_string()))?; - - let credentials = CredentialsFile::new_from_file(credentials_path) - .await - .map_err(|e| Error::Other(format!("Failed to load Google KMS credentials: {}", e)))?; - - let client_config = ClientConfig::default() - .with_credentials(credentials) - .await - .map_err(|e| Error::Other(format!("Failed to create Google KMS client: {}", e)))?; - Ok(Self { client_config }) - } -} - -#[derive(Clone, Debug)] -pub struct GcpKmsClient { - inner: Client, -} - -impl GcpKmsClient { - pub async fn new(config: GcpKmsConfig) -> Result { - let client = Client::new(config.client_config) - .await - .map_err(|e| Error::Other(format!("Failed to create Google KMS client: {}", e)))?; - - Ok(Self { inner: client }) - } - - pub fn inner(&self) -> &Client { - &self.inner - } -} From 52d0e3d7062a695f0195dc0fb71c8552b622eb0d Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 12:43:34 +0100 Subject: [PATCH 08/34] update --- e2e/Cargo.toml | 19 -- e2e/src/e2e_helpers.rs | 17 -- e2e/src/lib.rs | 17 +- packages/fuels-accounts/src/kms.rs | 1 - repomix-output.txt | 365 +++++++++++++++++++++++++++++ 5 files changed, 372 insertions(+), 47 deletions(-) create mode 100644 repomix-output.txt diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 6ce35105e..f6753b056 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -24,15 +24,7 @@ tai64 = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["test-util"] } -itertools = { workspace = true } -futures = { workspace = true } -rand = { workspace = true } portpicker = { workspace = true } - -fuel-core-chain-config = { workspace = true, features = [ - "std", - "test-helpers", -] } fuels-accounts = { workspace = true, features = ["test-helpers"] } fuel-core-types = { workspace = true, features = [ @@ -40,17 +32,10 @@ fuel-core-types = { workspace = true, features = [ ] } testcontainers = { workspace = true } -k256 = { workspace = true, features = ["ecdsa-core"] } -aws-config = { workspace = true, features = ["rustls"] } aws-sdk-kms = { workspace = true, features = ["rustls"] } fuel-core-client = { workspace = true } -google-cloud-kms = { version = "0.6.0"} -tonic = { version = "0.12.3", features = ["tls"] } -tonic-types = { version = "0.12.3" } - - [build-dependencies] anyhow = { workspace = true, features = ["std"] } flate2 = { workspace = true, features = ["zlib"] } @@ -68,7 +53,3 @@ coin-cache = ["fuels/coin-cache"] [dependencies] anyhow = "1.0.86" url = "2.5.2" -serde_json = "1.0.120" -reqwest = "0.12.5" -tokio-stream = "0.1.17" -async-stream = "0.3.5" \ No newline at end of file diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs index e2eaae3ed..53d9c01f5 100644 --- a/e2e/src/e2e_helpers.rs +++ b/e2e/src/e2e_helpers.rs @@ -1,4 +1,3 @@ -use crate::google_kms::{GoogleKms, GoogleKmsProcess}; use crate::{ aws_kms::{AwsKms, AwsKmsProcess, KmsTestKey}, fuel_node::{FuelNode, FuelNodeProcess}, @@ -8,10 +7,6 @@ pub async fn start_aws_kms(logs: bool) -> anyhow::Result { AwsKms::default().with_show_logs(logs).start().await } -pub async fn start_google_kms(logs: bool) -> anyhow::Result { - GoogleKms::default().with_show_logs(logs).start().await -} - pub async fn create_and_fund_aws_kms_key( kms: &AwsKmsProcess, fuel_node: &FuelNodeProcess, @@ -26,15 +21,3 @@ pub async fn create_and_fund_aws_kms_key( pub async fn start_fuel_node(logs: bool) -> anyhow::Result { FuelNode::default().with_show_logs(logs).start().await } - -pub async fn create_and_fund_google_kms_key( - kms: &GoogleKmsProcess, - fuel_node: &FuelNodeProcess, -) -> anyhow::Result<()> { - // let amount = 5_000_000_000; - // let key = kms.create_key().await?; - // let address = key.kms_key.address().clone(); - // fuel_node.fund(address, amount).await?; - - Ok(()) -} diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 18ed8c4e8..201d18d28 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -6,20 +6,14 @@ mod client; mod e2e_helpers; #[cfg(test)] mod fuel_node; -#[cfg(test)] -mod google_kms; #[cfg(test)] mod tests { - use crate::e2e_helpers::{ - create_and_fund_aws_kms_key, create_and_fund_google_kms_key, start_aws_kms, - start_fuel_node, start_google_kms, - }; + use crate::e2e_helpers::{create_and_fund_aws_kms_key, start_aws_kms, start_fuel_node}; use anyhow::Result; use fuels::prelude::{AssetId, Provider}; use fuels_accounts::kms::AwsWallet; use fuels_accounts::ViewOnlyAccount; - use google_cloud_kms::client::{Client, ClientConfig}; use std::str::FromStr; #[tokio::test(flavor = "multi_thread")] @@ -37,8 +31,12 @@ mod tests { AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") .expect("AssetId to be well formed"); - let provider = Provider::connect(fuel_node.url()).await?; - let wallet = AwsWallet::with_kms_key(kms_key.id, Some(provider)).await?; + let your_kms_key_id = kms_key.id; + let fuels_provider = Provider::connect(fuel_node.url()).await?; + + // ANCHOR: use_kms_wallet + let wallet = AwsWallet::with_kms_key(your_kms_key_id, Some(fuels_provider)).await?; + // ANCHOR_END: use_kms_wallet let founded_coins = wallet .get_coins(asset_id) @@ -97,5 +95,4 @@ mod tests { Ok(()) } - } diff --git a/packages/fuels-accounts/src/kms.rs b/packages/fuels-accounts/src/kms.rs index 283a2db18..886dde106 100644 --- a/packages/fuels-accounts/src/kms.rs +++ b/packages/fuels-accounts/src/kms.rs @@ -1,4 +1,3 @@ mod aws; -mod google; pub use aws::*; diff --git a/repomix-output.txt b/repomix-output.txt new file mode 100644 index 000000000..08f2c0887 --- /dev/null +++ b/repomix-output.txt @@ -0,0 +1,365 @@ +This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix. + +================================================================ +File Summary +================================================================ + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +File Format: +------------ +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A separator line (================) + b. The file path (File: path/to/file) + c. Another separator line + d. The full contents of the file + e. A blank line + +Usage Guidelines: +----------------- +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + +Notes: +------ +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Only files matching these patterns are included: docs/src/wallets +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded + +Additional Info: +---------------- + +================================================================ +Directory Structure +================================================================ +docs/ + src/ + wallets/ + access.md + checking-balances-and-coins.md + encrypting-and-storing.md + index.md + mnemonic-wallet.md + private-keys.md + signing.md + test-wallets.md + +================================================================ +Files +================================================================ + +================ +File: docs/src/wallets/access.md +================ +# Wallet Access + + + +The kinds of operations we can perform with a `Wallet` instance depend on +whether or not we have access to the wallet's private key. + +In order to differentiate between `Wallet` instances that know their private key +and those that do not, we use the `WalletUnlocked` and `Wallet` types +respectively. + + +## Wallet States + + + +The `WalletUnlocked` type represents a wallet whose private key is known and +stored internally in memory. A wallet must be of type `WalletUnlocked` in order +to perform operations that involve signing messages or +transactions. + +You can learn more about signing [here](./signing.md). + + + +The `Wallet` type represents a wallet whose private key is *not* known or stored +in memory. Instead, `Wallet` only knows its public address. A `Wallet` cannot be +used to sign transactions, however it may still perform a whole suite of useful +operations including listing transactions, assets, querying balances, and so on. + + +Note that the `WalletUnlocked` type provides a `Deref` implementation targeting +its inner `Wallet` type. This means that all methods available on the `Wallet` +type are also available on the `WalletUnlocked` type. In other words, +`WalletUnlocked` can be thought of as a thin wrapper around `Wallet` that +provides greater access via its private key. + +## Transitioning States + +A `Wallet` instance can be unlocked by providing the private key: + +```rust,ignore +let wallet_unlocked = wallet_locked.unlock(private_key); +``` + +A `WalletUnlocked` instance can be locked using the `lock` method: + +```rust,ignore +let wallet_locked = wallet_unlocked.lock(); +``` + +Most wallet constructors that create or generate a new wallet are provided on +the `WalletUnlocked` type. Consider locking the wallet with the `lock` method after the new private +key has been handled in order to reduce the scope in which the wallet's private +key is stored in memory. + +## Design Guidelines + +When designing APIs that accept a wallet as an input, we should think carefully +about the kind of access that we require. API developers should aim to minimise +their usage of `WalletUnlocked` in order to ensure private keys are stored in +memory no longer than necessary to reduce the surface area for attacks and +vulnerabilities in downstream libraries and applications. + +================ +File: docs/src/wallets/checking-balances-and-coins.md +================ +# Checking balances and coins + + + +In the Fuel network, each UTXO corresponds to a unique _coin_, and said _coin_ has a corresponding _amount_ (the same way a dollar bill has either 10$ or 5$ face value). So, when you want to query the balance for a given asset ID, you want to query the sum of the amount in each unspent coin. This querying is done very easily with a wallet: + + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:get_asset_balance}} +``` + + + +If you want to query all the balances (i.e., get the balance for each asset ID in that wallet), you can use the `get_balances` method: + + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:get_balances}} +``` + + + +The return type is a `HashMap`, where the key is the _asset ID's_ hex string, and the value is the corresponding balance. For example, we can get the base asset balance with: + + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:get_balance_hashmap}} +``` + +================ +File: docs/src/wallets/encrypting-and-storing.md +================ +# Encrypting and storing wallets + +## Creating a wallet and storing an encrypted JSON wallet on disk + +You can also manage a wallet using [JSON wallets](https://cryptobook.nakov.com/symmetric-key-ciphers/ethereum-wallet-encryption) that are securely encrypted and stored on the disk. This makes it easier to manage multiple wallets, especially for testing purposes. + +You can create a random wallet and, at the same time, encrypt and store it. Then, later, you can recover the wallet if you know the master password: + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:create_and_restore_json_wallet}} +``` + +## Encrypting and storing a wallet created from a mnemonic or private key + +If you have already created a wallet using a mnemonic phrase or a private key, you can also encrypt it and save it to disk: + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:create_and_store_mnemonic_wallet}} +``` + +================ +File: docs/src/wallets/index.md +================ +# Managing wallets + + + +You can use wallets for many important things, for instance: + +1. Checking your balance +2. Transferring coins to a destination address or contract +3. Signing messages and transactions +4. Paying for network fees when sending transactions or deploying smart contracts + + +The SDK gives you many different ways to create and access wallets. Let's explore these different approaches in the following sub-chapters. + + + +> **Note:** Keep in mind that you should never share your private/secret key. And in the case of wallets that were derived from a mnemonic phrase, never share your mnemonic phrase. If you're planning on storing the wallet on disk, do not store the plain private/secret key and do not store the plain mnemonic phrase. Instead, use `Wallet::encrypt` to encrypt its content first before saving it to disk. + + +================ +File: docs/src/wallets/mnemonic-wallet.md +================ +# Creating a wallet from mnemonic phrases + +A mnemonic phrase is a cryptographically-generated sequence of words that's used to derive a private key. For instance: `"oblige salon price punch saddle immune slogan rare snap desert retire surprise";` would generate the address `0xdf9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185`. + +In addition to that, we also support [Hierarchical Deterministic Wallets](https://www.ledger.com/academy/crypto/what-are-hierarchical-deterministic-hd-wallets) and [derivation paths](https://thebitcoinmanual.com/articles/btc-derivation-path/). You may recognize the string `"m/44'/60'/0'/0/0"` from somewhere; that's a derivation path. In simple terms, it's a way to derive many wallets from a single root wallet. + +The SDK gives you two wallets from mnemonic instantiation methods: one that takes a derivation path (`Wallet::new_from_mnemonic_phrase_with_path`) and one that uses the default derivation path, in case you don't want or don't need to configure that (`Wallet::new_from_mnemonic_phrase`). + +Here's how you can create wallets with both mnemonic phrases and derivation paths: + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:create_wallet_from_mnemonic}} +``` + +================ +File: docs/src/wallets/private-keys.md +================ +# Creating a wallet from a private key + +A new wallet with a randomly generated private key can be created by supplying `Option`. + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:create_random_wallet}} +``` + +Alternatively, you can create a wallet from a predefined `SecretKey`. + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:create_wallet_from_secret_key}} +``` + +> Note: if `None` is supplied instead of a provider, any transaction related to the wallet will result +> in an error until a provider is linked with `set_provider()`. The optional parameter +> enables defining owners (wallet addresses) of genesis coins before a provider is launched. + +================ +File: docs/src/wallets/signing.md +================ +# Signing + +Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with `wallet.sign`. Below is a full example of how to sign and recover a message. + +```rust,ignore +{{#include ../../../packages/fuels-accounts/src/account.rs:sign_message}} +``` + +## Adding `Signers` to a transaction builder + +Every signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signers in the transaction builder and resolve the final transaction automatically. This is done by storing signers until the final transaction is built. + +Below is a full example of how to create a transaction builder and add signers to it. + +> Note: When you add a `Signer` to a transaction builder, the signer is stored inside it and the transaction will not be resolved until you call `build()`! + +```rust,ignore +{{#include ../../../packages/fuels-accounts/src/account.rs:sign_tb}} +``` + +## Signing a built transaction + +If you have a built transaction and want to add a signature, you can use the `sign_with` method. + +```rust,ignore +{{#include ../../../e2e/tests/contracts.rs:tx_sign_with}} +``` + +================ +File: docs/src/wallets/test-wallets.md +================ +# Setting up test wallets + +You'll often want to create one or more test wallets when testing your contracts. Here's how to do it. + +## Setting up multiple test wallets + + + +If you need multiple test wallets, they can be set up using the `launch_custom_provider_and_get_wallets` method. + + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:multiple_wallets_helper}} +``` + + + +You can customize your test wallets via `WalletsConfig`. + + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:setup_5_wallets}} +``` + + + +>**Note** Wallets generated with `launch_provider_and_get_wallet` or `launch_custom_provider_and_get_wallets` +will have deterministic addresses. + + +## Setting up a test wallet with multiple random assets + +You can create a test wallet containing multiple assets (including the base asset to pay for gas). + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:multiple_assets_wallet}} +``` + +- coins: `Vec<(UtxoId, Coin)>` has `num_assets` * `coins_per_assets` coins (UTXOs) +- asset_ids: `Vec` contains the `num_assets` randomly generated `AssetId`s (always includes the base asset) + +## Setting up a test wallet with multiple custom assets + +You can also create assets with specific `AssetId`s, coin amounts, and number of coins. + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:custom_assets_wallet}} +``` + +This can also be achieved directly with the `WalletsConfig`. + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:custom_assets_wallet_short}} +``` + +>**Note** In this case, you need to manually add the base asset and the corresponding number of +>coins and coin amount + +## Setting up assets + +The Fuel blockchain holds many different assets; you can create your asset with its unique `AssetId` or create random assets for testing purposes. + +You can use only one asset to pay for transaction fees and gas: the base asset, whose `AssetId` is `0x000...0`, a 32-byte zeroed value. + +For testing purposes, you can configure coins and amounts for assets. You can use `setup_multiple_assets_coins`: + +```rust,ignore +{{#include ../../../examples/wallets/src/lib.rs:multiple_assets_coins}} +``` + +>**Note** If setting up multiple assets, one of these assets will always be the base asset. + +If you want to create coins only with the base asset, then you can use: + +```rust,ignore +{{#include ../../../examples/providers/src/lib.rs:setup_single_asset}} +``` + +>**Note** Choosing a large number of coins and assets for `setup_multiple_assets_coins` or `setup_single_asset_coins` can lead to considerable runtime for these methods. This will be improved in the future but for now, we recommend using up to **1_000_000** coins, or **1000** coins and assets simultaneously. + + + +================================================================ +End of Codebase +================================================================ From 6950675b60444958dd0b98caf03bb3d55a3237e0 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 14:01:32 +0100 Subject: [PATCH 09/34] update --- docs/spell-check-custom-words.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/spell-check-custom-words.txt b/docs/spell-check-custom-words.txt index 9b22e581a..e8c14a0d9 100644 --- a/docs/spell-check-custom-words.txt +++ b/docs/spell-check-custom-words.txt @@ -1,3 +1,4 @@ +KMS ABI ABIs ASM From a4ca73edf90edf8e619338b804a4b3dc38025298 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 14:01:57 +0100 Subject: [PATCH 10/34] update --- repomix-output.txt | 365 --------------------------------------------- 1 file changed, 365 deletions(-) delete mode 100644 repomix-output.txt diff --git a/repomix-output.txt b/repomix-output.txt deleted file mode 100644 index 08f2c0887..000000000 --- a/repomix-output.txt +++ /dev/null @@ -1,365 +0,0 @@ -This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix. - -================================================================ -File Summary -================================================================ - -Purpose: --------- -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - -File Format: ------------- -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Multiple file entries, each consisting of: - a. A separator line (================) - b. The file path (File: path/to/file) - c. Another separator line - d. The full contents of the file - e. A blank line - -Usage Guidelines: ------------------ -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - -Notes: ------- -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Only files matching these patterns are included: docs/src/wallets -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded - -Additional Info: ----------------- - -================================================================ -Directory Structure -================================================================ -docs/ - src/ - wallets/ - access.md - checking-balances-and-coins.md - encrypting-and-storing.md - index.md - mnemonic-wallet.md - private-keys.md - signing.md - test-wallets.md - -================================================================ -Files -================================================================ - -================ -File: docs/src/wallets/access.md -================ -# Wallet Access - - - -The kinds of operations we can perform with a `Wallet` instance depend on -whether or not we have access to the wallet's private key. - -In order to differentiate between `Wallet` instances that know their private key -and those that do not, we use the `WalletUnlocked` and `Wallet` types -respectively. - - -## Wallet States - - - -The `WalletUnlocked` type represents a wallet whose private key is known and -stored internally in memory. A wallet must be of type `WalletUnlocked` in order -to perform operations that involve signing messages or -transactions. - -You can learn more about signing [here](./signing.md). - - - -The `Wallet` type represents a wallet whose private key is *not* known or stored -in memory. Instead, `Wallet` only knows its public address. A `Wallet` cannot be -used to sign transactions, however it may still perform a whole suite of useful -operations including listing transactions, assets, querying balances, and so on. - - -Note that the `WalletUnlocked` type provides a `Deref` implementation targeting -its inner `Wallet` type. This means that all methods available on the `Wallet` -type are also available on the `WalletUnlocked` type. In other words, -`WalletUnlocked` can be thought of as a thin wrapper around `Wallet` that -provides greater access via its private key. - -## Transitioning States - -A `Wallet` instance can be unlocked by providing the private key: - -```rust,ignore -let wallet_unlocked = wallet_locked.unlock(private_key); -``` - -A `WalletUnlocked` instance can be locked using the `lock` method: - -```rust,ignore -let wallet_locked = wallet_unlocked.lock(); -``` - -Most wallet constructors that create or generate a new wallet are provided on -the `WalletUnlocked` type. Consider locking the wallet with the `lock` method after the new private -key has been handled in order to reduce the scope in which the wallet's private -key is stored in memory. - -## Design Guidelines - -When designing APIs that accept a wallet as an input, we should think carefully -about the kind of access that we require. API developers should aim to minimise -their usage of `WalletUnlocked` in order to ensure private keys are stored in -memory no longer than necessary to reduce the surface area for attacks and -vulnerabilities in downstream libraries and applications. - -================ -File: docs/src/wallets/checking-balances-and-coins.md -================ -# Checking balances and coins - - - -In the Fuel network, each UTXO corresponds to a unique _coin_, and said _coin_ has a corresponding _amount_ (the same way a dollar bill has either 10$ or 5$ face value). So, when you want to query the balance for a given asset ID, you want to query the sum of the amount in each unspent coin. This querying is done very easily with a wallet: - - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:get_asset_balance}} -``` - - - -If you want to query all the balances (i.e., get the balance for each asset ID in that wallet), you can use the `get_balances` method: - - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:get_balances}} -``` - - - -The return type is a `HashMap`, where the key is the _asset ID's_ hex string, and the value is the corresponding balance. For example, we can get the base asset balance with: - - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:get_balance_hashmap}} -``` - -================ -File: docs/src/wallets/encrypting-and-storing.md -================ -# Encrypting and storing wallets - -## Creating a wallet and storing an encrypted JSON wallet on disk - -You can also manage a wallet using [JSON wallets](https://cryptobook.nakov.com/symmetric-key-ciphers/ethereum-wallet-encryption) that are securely encrypted and stored on the disk. This makes it easier to manage multiple wallets, especially for testing purposes. - -You can create a random wallet and, at the same time, encrypt and store it. Then, later, you can recover the wallet if you know the master password: - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:create_and_restore_json_wallet}} -``` - -## Encrypting and storing a wallet created from a mnemonic or private key - -If you have already created a wallet using a mnemonic phrase or a private key, you can also encrypt it and save it to disk: - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:create_and_store_mnemonic_wallet}} -``` - -================ -File: docs/src/wallets/index.md -================ -# Managing wallets - - - -You can use wallets for many important things, for instance: - -1. Checking your balance -2. Transferring coins to a destination address or contract -3. Signing messages and transactions -4. Paying for network fees when sending transactions or deploying smart contracts - - -The SDK gives you many different ways to create and access wallets. Let's explore these different approaches in the following sub-chapters. - - - -> **Note:** Keep in mind that you should never share your private/secret key. And in the case of wallets that were derived from a mnemonic phrase, never share your mnemonic phrase. If you're planning on storing the wallet on disk, do not store the plain private/secret key and do not store the plain mnemonic phrase. Instead, use `Wallet::encrypt` to encrypt its content first before saving it to disk. - - -================ -File: docs/src/wallets/mnemonic-wallet.md -================ -# Creating a wallet from mnemonic phrases - -A mnemonic phrase is a cryptographically-generated sequence of words that's used to derive a private key. For instance: `"oblige salon price punch saddle immune slogan rare snap desert retire surprise";` would generate the address `0xdf9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185`. - -In addition to that, we also support [Hierarchical Deterministic Wallets](https://www.ledger.com/academy/crypto/what-are-hierarchical-deterministic-hd-wallets) and [derivation paths](https://thebitcoinmanual.com/articles/btc-derivation-path/). You may recognize the string `"m/44'/60'/0'/0/0"` from somewhere; that's a derivation path. In simple terms, it's a way to derive many wallets from a single root wallet. - -The SDK gives you two wallets from mnemonic instantiation methods: one that takes a derivation path (`Wallet::new_from_mnemonic_phrase_with_path`) and one that uses the default derivation path, in case you don't want or don't need to configure that (`Wallet::new_from_mnemonic_phrase`). - -Here's how you can create wallets with both mnemonic phrases and derivation paths: - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:create_wallet_from_mnemonic}} -``` - -================ -File: docs/src/wallets/private-keys.md -================ -# Creating a wallet from a private key - -A new wallet with a randomly generated private key can be created by supplying `Option`. - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:create_random_wallet}} -``` - -Alternatively, you can create a wallet from a predefined `SecretKey`. - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:create_wallet_from_secret_key}} -``` - -> Note: if `None` is supplied instead of a provider, any transaction related to the wallet will result -> in an error until a provider is linked with `set_provider()`. The optional parameter -> enables defining owners (wallet addresses) of genesis coins before a provider is launched. - -================ -File: docs/src/wallets/signing.md -================ -# Signing - -Once you've instantiated your wallet in an unlocked state using one of the previously discussed methods, you can sign a message with `wallet.sign`. Below is a full example of how to sign and recover a message. - -```rust,ignore -{{#include ../../../packages/fuels-accounts/src/account.rs:sign_message}} -``` - -## Adding `Signers` to a transaction builder - -Every signed resource in the inputs needs to have a witness index that points to a valid witness. Changing the witness index inside an input will change the transaction ID. This means that we need to set all witness indexes before finally signing the transaction. Previously, the user had to make sure that the witness indexes and the order of the witnesses are correct. To automate this process, the SDK will keep track of the signers in the transaction builder and resolve the final transaction automatically. This is done by storing signers until the final transaction is built. - -Below is a full example of how to create a transaction builder and add signers to it. - -> Note: When you add a `Signer` to a transaction builder, the signer is stored inside it and the transaction will not be resolved until you call `build()`! - -```rust,ignore -{{#include ../../../packages/fuels-accounts/src/account.rs:sign_tb}} -``` - -## Signing a built transaction - -If you have a built transaction and want to add a signature, you can use the `sign_with` method. - -```rust,ignore -{{#include ../../../e2e/tests/contracts.rs:tx_sign_with}} -``` - -================ -File: docs/src/wallets/test-wallets.md -================ -# Setting up test wallets - -You'll often want to create one or more test wallets when testing your contracts. Here's how to do it. - -## Setting up multiple test wallets - - - -If you need multiple test wallets, they can be set up using the `launch_custom_provider_and_get_wallets` method. - - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:multiple_wallets_helper}} -``` - - - -You can customize your test wallets via `WalletsConfig`. - - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:setup_5_wallets}} -``` - - - ->**Note** Wallets generated with `launch_provider_and_get_wallet` or `launch_custom_provider_and_get_wallets` -will have deterministic addresses. - - -## Setting up a test wallet with multiple random assets - -You can create a test wallet containing multiple assets (including the base asset to pay for gas). - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:multiple_assets_wallet}} -``` - -- coins: `Vec<(UtxoId, Coin)>` has `num_assets` * `coins_per_assets` coins (UTXOs) -- asset_ids: `Vec` contains the `num_assets` randomly generated `AssetId`s (always includes the base asset) - -## Setting up a test wallet with multiple custom assets - -You can also create assets with specific `AssetId`s, coin amounts, and number of coins. - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:custom_assets_wallet}} -``` - -This can also be achieved directly with the `WalletsConfig`. - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:custom_assets_wallet_short}} -``` - ->**Note** In this case, you need to manually add the base asset and the corresponding number of ->coins and coin amount - -## Setting up assets - -The Fuel blockchain holds many different assets; you can create your asset with its unique `AssetId` or create random assets for testing purposes. - -You can use only one asset to pay for transaction fees and gas: the base asset, whose `AssetId` is `0x000...0`, a 32-byte zeroed value. - -For testing purposes, you can configure coins and amounts for assets. You can use `setup_multiple_assets_coins`: - -```rust,ignore -{{#include ../../../examples/wallets/src/lib.rs:multiple_assets_coins}} -``` - ->**Note** If setting up multiple assets, one of these assets will always be the base asset. - -If you want to create coins only with the base asset, then you can use: - -```rust,ignore -{{#include ../../../examples/providers/src/lib.rs:setup_single_asset}} -``` - ->**Note** Choosing a large number of coins and assets for `setup_multiple_assets_coins` or `setup_single_asset_coins` can lead to considerable runtime for these methods. This will be improved in the future but for now, we recommend using up to **1_000_000** coins, or **1000** coins and assets simultaneously. - - - -================================================================ -End of Codebase -================================================================ From d97edf4edb267799afa1a73d937a9c6dc6cc17fe Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 14:04:52 +0100 Subject: [PATCH 11/34] update --- docs/src/wallets/kms-wallets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/wallets/kms-wallets.md b/docs/src/wallets/kms-wallets.md index b335b6ac0..d5e9a0dbf 100644 --- a/docs/src/wallets/kms-wallets.md +++ b/docs/src/wallets/kms-wallets.md @@ -4,4 +4,4 @@ AWS Key Management Service (KMS) provides a secure way to manage cryptographic k ```rust,ignore {{#include ../../../e2e/src/lib.rs:use_kms_wallet}} -``` \ No newline at end of file +``` From 141178ba2b042ee5967873b5ddd190ae5e7cac73 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 14:44:20 +0100 Subject: [PATCH 12/34] update --- packages/fuels-accounts/Cargo.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 531f97432..c779bd77c 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -28,19 +28,14 @@ tai64 = { workspace = true, features = ["serde"] } thiserror = { workspace = true, default-features = false } tokio = { workspace = true, features = ["full"], optional = true } zeroize = { workspace = true, features = ["derive"] } - k256 = { workspace = true, features = ["ecdsa-core"] } + # AWS KMS client aws-config = { workspace = true, features = [ "behavior-version-latest", ] } aws-sdk-kms = { workspace = true, features = ["default"] } -# Google Cloud KMS client -google-cloud-kms = "0.6.0" -google-cloud-auth = "0.17.2" -hyper-rustls = "0.27.5" - [dev-dependencies] mockall = { workspace = true, default-features = false } fuel-tx = { workspace = true, features = ["test-helpers", "random"] } From 71cc9652c291a273ba4eeac25e7730c3edac27bd Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 14:45:01 +0100 Subject: [PATCH 13/34] update --- _typos.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_typos.toml b/_typos.toml index d991235a6..2383a8139 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,2 +1,5 @@ [files] extend-exclude = ["packages/fuels-accounts/src/schema/schema.sdl"] + +[default] +extend-words = ["KMS", "kms", "Kms"] From 1715c3781a4308a2620a18912564b1db1ad82f56 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 14:55:50 +0100 Subject: [PATCH 14/34] update --- _typos.toml | 5 +---- docs/spell-check-custom-words.txt | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/_typos.toml b/_typos.toml index 2383a8139..f908ff732 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,5 +1,2 @@ [files] -extend-exclude = ["packages/fuels-accounts/src/schema/schema.sdl"] - -[default] -extend-words = ["KMS", "kms", "Kms"] +extend-exclude = ["packages/fuels-accounts/src/schema/schema.sdl"] \ No newline at end of file diff --git a/docs/spell-check-custom-words.txt b/docs/spell-check-custom-words.txt index e8c14a0d9..99fc4ef85 100644 --- a/docs/spell-check-custom-words.txt +++ b/docs/spell-check-custom-words.txt @@ -1,4 +1,6 @@ KMS +kms +Kms ABI ABIs ASM From 91a9870f2568ce38fbffd9e6d26ad9adec6f56ec Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 15:51:33 +0100 Subject: [PATCH 15/34] update --- _typos.toml => typos.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename _typos.toml => typos.toml (100%) diff --git a/_typos.toml b/typos.toml similarity index 100% rename from _typos.toml rename to typos.toml From da5f359c98931ad39543bfc7de4ee2eee8a88c89 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 24 Feb 2025 15:51:47 +0100 Subject: [PATCH 16/34] update --- .github/workflows/ci.yml | 4 +- Cargo.toml | 8 +- typos.toml => _typos.toml | 0 e2e/Cargo.toml | 26 ++---- e2e/src/aws_kms.rs | 4 +- e2e/src/client.rs | 33 ------- e2e/src/e2e_helpers.rs | 28 +++--- e2e/src/fuel_node.rs | 103 ---------------------- e2e/src/lib.rs | 98 +-------------------- e2e/tests/aws.rs | 114 +++++++++++++++++++++++++ packages/fuels-accounts/Cargo.toml | 16 ++-- packages/fuels-programs/Cargo.toml | 2 +- packages/fuels-test-helpers/Cargo.toml | 2 +- wasm-tests/Cargo.toml | 2 +- 14 files changed, 155 insertions(+), 285 deletions(-) rename typos.toml => _typos.toml (100%) delete mode 100644 e2e/src/client.rs delete mode 100644 e2e/src/fuel_node.rs create mode 100644 e2e/tests/aws.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17e951276..aff6727ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,7 +277,9 @@ jobs: - name: Check for typos if: ${{ matrix.command == 'check_typos' }} - uses: crate-ci/typos@v1.20.3 + uses: crate-ci/typos@v1.29.5 + with: + config: ./typos.toml publish: needs: diff --git a/Cargo.toml b/Cargo.toml index 165782155..55f946626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,10 +89,10 @@ dotenv = { version = "0.15", default-features = false } toml = { version = "0.8", default-features = false } mockall = { version = "0.13", default-features = false } -aws-config = { version = "1.5.5", default-features = false } -aws-sdk-kms = { version = "1.36", default-features = false } -testcontainers = { version = "0.20", default-features = false } -k256 = { version = "0.13.3", default-features = false } +aws-config = { version = "1", default-features = false } +aws-sdk-kms = { version = "1", default-features = false } +testcontainers = { version = "0.23", default-features = false } +k256 = { version = "0.13", default-features = false } # Dependencies from the `fuel-core` repository: fuel-core = { version = "0.41.7", default-features = false, features = [ diff --git a/typos.toml b/_typos.toml similarity index 100% rename from typos.toml rename to _typos.toml diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index f6753b056..8e5ca867b 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -18,22 +18,10 @@ chrono = { workspace = true } fuel-asm = { workspace = true } # TODO: [issue](https://github.com/FuelLabs/fuels-rs/issues/1375) needs to be removed, `ScriptTransaction` and `CreateTransaction` in `fuels` use `fuel_tx::Input` but don't reexport or convert it into a `fuels` owned type fuel-tx = { workspace = true } -fuels = { workspace = true } +fuels-accounts = { workspace = true, features = ["test-helpers"] } # used in test assertions tai64 = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["test-util"] } - -portpicker = { workspace = true } -fuels-accounts = { workspace = true, features = ["test-helpers"] } - -fuel-core-types = { workspace = true, features = [ - "da-compression", -] } - -testcontainers = { workspace = true } -aws-sdk-kms = { workspace = true, features = ["rustls"] } -fuel-core-client = { workspace = true } [build-dependencies] @@ -45,11 +33,15 @@ reqwest = { workspace = true, features = ["blocking", "default-tls"] } semver = { workspace = true } tar = { workspace = true } +[dependencies] +testcontainers = { workspace = true } +fuels = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true, features = ["test-util"]} +aws-sdk-kms = { workspace = true, features = ["rustls"] } + [features] -default = ["fuels/default", "coin-cache"] +default = ["fuels/default", "coin-cache", ] fuel-core-lib = ["fuels/fuel-core-lib"] rocksdb = ["fuels/rocksdb"] coin-cache = ["fuels/coin-cache"] -[dependencies] -anyhow = "1.0.86" -url = "2.5.2" diff --git a/e2e/src/aws_kms.rs b/e2e/src/aws_kms.rs index 6f411167e..683147140 100644 --- a/e2e/src/aws_kms.rs +++ b/e2e/src/aws_kms.rs @@ -1,6 +1,5 @@ use anyhow::Context; -use fuels::accounts::kms::{AwsClient, AwsConfig}; -use fuels_accounts::kms::KmsKey; +use fuels::accounts::kms::{AwsClient, AwsConfig, KmsKey}; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; @@ -115,7 +114,6 @@ impl AwsKmsProcess { .send() .await?; - // use arn as id to closer imitate prod behavior let id = response .key_metadata .and_then(|metadata| metadata.arn) diff --git a/e2e/src/client.rs b/e2e/src/client.rs deleted file mode 100644 index 7cf86eef9..000000000 --- a/e2e/src/client.rs +++ /dev/null @@ -1,33 +0,0 @@ -use url::Url; - -use fuel_core_client::client::FuelClient; -use fuels::types::errors::Error; -use fuels::types::errors::Result; -#[derive(Clone)] -pub struct HttpClient { - client: FuelClient, -} - -impl HttpClient { - #[must_use] - pub fn new(url: &Url) -> Self { - let client = FuelClient::new(url).expect("Url to be well formed"); - Self { client } - } - - pub async fn produce_blocks(&self, num: u32) -> Result<()> { - self.client - .produce_blocks(num, None) - .await - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) - } - - pub async fn health(&self) -> Result { - match self.client.health().await { - Ok(healthy) => Ok(healthy), - Err(err) => Err(Error::Other(err.to_string())), - } - } -} diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs index 53d9c01f5..0321ea4e3 100644 --- a/e2e/src/e2e_helpers.rs +++ b/e2e/src/e2e_helpers.rs @@ -1,23 +1,19 @@ use crate::{ - aws_kms::{AwsKms, AwsKmsProcess, KmsTestKey}, - fuel_node::{FuelNode, FuelNodeProcess}, + aws_kms::{AwsKms, AwsKmsProcess}, }; pub async fn start_aws_kms(logs: bool) -> anyhow::Result { AwsKms::default().with_show_logs(logs).start().await } -pub async fn create_and_fund_aws_kms_key( - kms: &AwsKmsProcess, - fuel_node: &FuelNodeProcess, -) -> anyhow::Result { - let amount = 5_000_000_000; - let key = kms.create_key().await?; - let address = key.kms_key.address().clone(); - fuel_node.fund(address, amount).await?; - - Ok(key) -} -pub async fn start_fuel_node(logs: bool) -> anyhow::Result { - FuelNode::default().with_show_logs(logs).start().await -} +// pub async fn create_and_fund_aws_kms_key( +// kms: &FuelService, +// fuel_node: &FuelNodeProcess, +// ) -> anyhow::Result { +// let amount = 5_000_000_000; +// let key = kms.create_key().await?; +// let address = key.kms_key.address().clone(); +// fuel_node.fund(&address, amount).await?; +// +// Ok(key) +// } diff --git a/e2e/src/fuel_node.rs b/e2e/src/fuel_node.rs deleted file mode 100644 index 081c0cbf2..000000000 --- a/e2e/src/fuel_node.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::client::HttpClient; -use anyhow::Context; -use fuel_core_types::fuel_tx::AssetId; -use fuels::accounts::Account; -use fuels::crypto::SecretKey; -use fuels::prelude::{Bech32Address, Provider, TxPolicies, WalletUnlocked}; -use std::str::FromStr; -use url::Url; - -#[derive(Default, Debug)] -pub struct FuelNode { - show_logs: bool, -} - -pub struct FuelNodeProcess { - _child: tokio::process::Child, - url: Url, -} - -impl FuelNode { - pub async fn start(&self) -> anyhow::Result { - let unused_port = portpicker::pick_unused_port() - .ok_or_else(|| anyhow::anyhow!("No free port to start fuel-core"))?; - - let mut cmd = tokio::process::Command::new("fuel-core"); - - cmd.arg("run") - .arg("--port") - .arg(unused_port.to_string()) - .arg("--db-type") - .arg("in-memory") - .arg("--debug") - .kill_on_drop(true) - .stdin(std::process::Stdio::null()); - - let sink = if self.show_logs { - std::process::Stdio::inherit - } else { - std::process::Stdio::null - }; - cmd.stdout(sink()).stderr(sink()); - - let child = cmd.spawn()?; - - let url = format!("http://localhost:{}", unused_port).parse()?; - - let process = FuelNodeProcess { _child: child, url }; - - process.wait_until_healthy().await; - - Ok(process) - } - - pub fn with_show_logs(mut self, show_logs: bool) -> Self { - self.show_logs = show_logs; - self - } -} - -impl FuelNodeProcess { - pub fn client(&self) -> HttpClient { - HttpClient::new(&self.url) - } - - async fn wait_until_healthy(&self) { - loop { - if let Ok(true) = self.client().health().await { - break; - } - } - } - - pub fn url(&self) -> &Url { - &self.url - } - - pub async fn fund(&self, address: Bech32Address, amount: u64) -> anyhow::Result<()> { - let fuels_provider = Provider::connect(self.url()).await?; - - // Create a wallet with the private key of the default account - let mut default_wallet = WalletUnlocked::new_from_private_key( - SecretKey::from_str( - "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c", - )?, - None, - ); - default_wallet.set_provider(fuels_provider.clone()); - - // Transfer ETH funds to the AWS wallet from the default wallet - let asset_id = - AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") - .expect("AssetId to be well formed"); - - default_wallet - .transfer(&address, amount, asset_id, TxPolicies::default()) - .await - .context("Failed to transfer funds")?; - - self.client().produce_blocks(1).await?; - - Ok(()) - } -} diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 201d18d28..ea443026c 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -1,98 +1,2 @@ -#[cfg(test)] mod aws_kms; -#[cfg(test)] -mod client; -#[cfg(test)] -mod e2e_helpers; -#[cfg(test)] -mod fuel_node; - -#[cfg(test)] -mod tests { - use crate::e2e_helpers::{create_and_fund_aws_kms_key, start_aws_kms, start_fuel_node}; - use anyhow::Result; - use fuels::prelude::{AssetId, Provider}; - use fuels_accounts::kms::AwsWallet; - use fuels_accounts::ViewOnlyAccount; - use std::str::FromStr; - - #[tokio::test(flavor = "multi_thread")] - async fn fund_aws_wallet() -> Result<()> { - let kms = start_aws_kms(false).await?; - let fuel_node = start_fuel_node(false).await?; - let kms_key = create_and_fund_aws_kms_key(&kms, &fuel_node).await?; - - std::env::set_var("AWS_ACCESS_KEY_ID", "test"); - std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); - std::env::set_var("AWS_REGION", "us-east-1"); - std::env::set_var("AWS_ENDPOINT_URL", &kms_key.url); - - let asset_id = - AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") - .expect("AssetId to be well formed"); - - let your_kms_key_id = kms_key.id; - let fuels_provider = Provider::connect(fuel_node.url()).await?; - - // ANCHOR: use_kms_wallet - let wallet = AwsWallet::with_kms_key(your_kms_key_id, Some(fuels_provider)).await?; - // ANCHOR_END: use_kms_wallet - - let founded_coins = wallet - .get_coins(asset_id) - .await? - .first() - .expect("No coins found") - .amount; - assert_eq!(founded_coins, 5000000000); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread")] - async fn deploy_contract() -> anyhow::Result<()> { - use fuels::prelude::*; - - let kms = start_aws_kms(false).await?; - let fuel_node = start_fuel_node(false).await?; - let kms_key = create_and_fund_aws_kms_key(&kms, &fuel_node).await?; - - std::env::set_var("AWS_ACCESS_KEY_ID", "test"); - std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); - std::env::set_var("AWS_REGION", "us-east-1"); - std::env::set_var("AWS_ENDPOINT_URL", &kms_key.url); - - let asset_id = - AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") - .expect("AssetId to be well formed"); - - let provider = Provider::connect(fuel_node.url()).await?; - let wallet = AwsWallet::with_kms_key(kms_key.id, Some(provider)).await?; - - let founded_coins = wallet - .get_coins(asset_id) - .await? - .first() - .expect("No coins found") - .amount; - assert_eq!(founded_coins, 5000000000); - - let contract_id = Contract::load_from( - "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", - LoadConfiguration::default(), - )? - .deploy(&wallet, TxPolicies::default()) - .await?; - - println!("Contract deployed @ {contract_id}"); - - let founded_coins = wallet - .get_coins(asset_id) - .await? - .first() - .expect("No coins found") - .amount; - assert_eq!(founded_coins, 4999983198); - - Ok(()) - } -} +pub mod e2e_helpers; diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs new file mode 100644 index 000000000..0cd381107 --- /dev/null +++ b/e2e/tests/aws.rs @@ -0,0 +1,114 @@ + +#[cfg(test)] +mod tests { + use anyhow::Result; + use e2e::e2e_helpers::start_aws_kms; + use fuels::prelude::{launch_custom_provider_and_get_wallets, AssetId, Contract, LoadConfiguration, TxPolicies, WalletsConfig}; + use fuels::types::errors::Context; + use fuels_accounts::kms::AwsWallet; + use fuels_accounts::{Account, ViewOnlyAccount}; + + #[tokio::test(flavor = "multi_thread")] + async fn fund_aws_wallet() -> Result<()> { + let kms = start_aws_kms(false).await?; + + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new(Some(1), None, None), + None, + None, + ) + .await?; + let wallet = wallets.first_mut().expect("No wallets found"); + + let amount = 500000000; + let key = kms.create_key().await?; + let address = key.kms_key.address().clone(); + + wallet + .transfer(&address, amount, AssetId::zeroed(), TxPolicies::default()) + .await + .context("Failed to transfer funds")?; + + std::env::set_var("AWS_ACCESS_KEY_ID", "test"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); + std::env::set_var("AWS_REGION", "us-east-1"); + std::env::set_var("AWS_ENDPOINT_URL", &key.url); + + let your_kms_key_id = key.id; + let provider = wallet.provider().expect("No provider found").clone(); + + // ANCHOR: use_kms_wallet + let wallet = AwsWallet::with_kms_key(your_kms_key_id, Some(provider)).await?; + // ANCHOR_END: use_kms_wallet + + let founded_coins = wallet + .get_coins(AssetId::zeroed()) + .await? + .first() + .expect("No coins found") + .amount; + assert_eq!(founded_coins, 500000000); + Ok(()) + } + + #[tokio::test(flavor = "multi_thread")] + async fn deploy_contract() -> anyhow::Result<()> { + let kms = start_aws_kms(false).await?; + + let mut wallets = launch_custom_provider_and_get_wallets( + WalletsConfig::new(Some(1), None, None), + None, + None, + ) + .await?; + let wallet = wallets.first_mut().expect("No wallets found"); + + let amount = 500000000; + let key = kms.create_key().await?; + let address = key.kms_key.address().clone(); + + wallet + .transfer(&address, amount, AssetId::zeroed(), TxPolicies::default()) + .await + .context("Failed to transfer funds")?; + + std::env::set_var("AWS_ACCESS_KEY_ID", "test"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); + std::env::set_var("AWS_REGION", "us-east-1"); + std::env::set_var("AWS_ENDPOINT_URL", &key.url); + + let your_kms_key_id = key.id; + let provider = wallet.provider().expect("No provider found").clone(); + + let aws_wallet = AwsWallet::with_kms_key(your_kms_key_id, Some(provider)).await?; + + + let founded_coins = aws_wallet + .get_coins(AssetId::zeroed()) + .await? + .first() + .expect("No coins found") + .amount; + assert_eq!(founded_coins, 500000000); + + + let contract_id = Contract::load_from( + "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", + LoadConfiguration::default(), + )? + .deploy(&aws_wallet, TxPolicies::default()) + .await?; + + // println!("Contract deployed @ {contract_id}"); + // + // let founded_coins = wallet + // .get_coins(AssetId::zeroed()) + // .await? + // .first() + // .expect("No coins found") + // .amount; + // assert_eq!(founded_coins, 499998321); + + Ok(()) + } +} diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index c779bd77c..2b73f4106 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -11,6 +11,12 @@ description = "Fuel Rust SDK accounts." [dependencies] async-trait = { workspace = true, default-features = false } + +# AWS KMS client +aws-config = { workspace = true, features = [ + "behavior-version-latest", +] } +aws-sdk-kms = { workspace = true, features = ["default"] } chrono = { workspace = true } cynic = { workspace = true, optional = true } elliptic-curve = { workspace = true, default-features = false } @@ -22,23 +28,17 @@ fuel-tx = { workspace = true } fuel-types = { workspace = true, features = ["random"] } fuels-core = { workspace = true, default-features = false } itertools = { workspace = true } +k256 = { workspace = true, features = ["ecdsa-core"] } rand = { workspace = true, default-features = false } semver = { workspace = true } tai64 = { workspace = true, features = ["serde"] } thiserror = { workspace = true, default-features = false } tokio = { workspace = true, features = ["full"], optional = true } zeroize = { workspace = true, features = ["derive"] } -k256 = { workspace = true, features = ["ecdsa-core"] } - -# AWS KMS client -aws-config = { workspace = true, features = [ - "behavior-version-latest", -] } -aws-sdk-kms = { workspace = true, features = ["default"] } [dev-dependencies] -mockall = { workspace = true, default-features = false } fuel-tx = { workspace = true, features = ["test-helpers", "random"] } +mockall = { workspace = true, default-features = false } tempfile = { workspace = true } tokio = { workspace = true, features = ["test-util"] } diff --git a/packages/fuels-programs/Cargo.toml b/packages/fuels-programs/Cargo.toml index cbea44bb6..91011dcda 100644 --- a/packages/fuels-programs/Cargo.toml +++ b/packages/fuels-programs/Cargo.toml @@ -23,8 +23,8 @@ serde_json = { workspace = true } tokio = { workspace = true } [dev-dependencies] -test-case = { workspace = true } tempfile = "3.8.1" +test-case = { workspace = true } [features] default = ["std"] diff --git a/packages/fuels-test-helpers/Cargo.toml b/packages/fuels-test-helpers/Cargo.toml index 7a41ceab2..5d3db165b 100644 --- a/packages/fuels-test-helpers/Cargo.toml +++ b/packages/fuels-test-helpers/Cargo.toml @@ -17,8 +17,8 @@ fuel-core = { workspace = true, default-features = false, features = [ fuel-core-chain-config = { workspace = true, features = ["test-helpers"] } fuel-core-client = { workspace = true } fuel-core-poa = { workspace = true } -fuel-core-types = { workspace = true } fuel-core-services = { workspace = true } +fuel-core-types = { workspace = true } fuel-crypto = { workspace = true } fuel-tx = { workspace = true } fuel-types = { workspace = true, features = ["random"] } diff --git a/wasm-tests/Cargo.toml b/wasm-tests/Cargo.toml index df171ce68..8d9426724 100644 --- a/wasm-tests/Cargo.toml +++ b/wasm-tests/Cargo.toml @@ -13,8 +13,8 @@ rust-version = { workspace = true } crate-type = ['cdylib'] [dev-dependencies] -hex = { workspace = true } fuels = { workspace = true } fuels-core = { workspace = true } getrandom = { version = "0.2.11", features = ["js"] } +hex = { workspace = true } wasm-bindgen-test = "0.3.39" From 911cb1d92f9f877626eb31fa832ecd9a483cc3ff Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Wed, 26 Feb 2025 16:57:58 +0100 Subject: [PATCH 17/34] update --- e2e/Cargo.toml | 1 + e2e/tests/aws.rs | 6 ++---- packages/fuels-accounts/Cargo.toml | 5 +++-- packages/fuels-accounts/src/lib.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 3df7576ef..f984eb1d7 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -36,6 +36,7 @@ tar = { workspace = true } anyhow = { workspace = true } aws-sdk-kms = { workspace = true, features = ["rustls"] } fuels = { workspace = true } +fuels-accounts = { workspace = true, features = ["std", "kms_signer", "test-helpers"] } testcontainers = { workspace = true } tokio = { workspace = true, features = ["test-util"] } diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index 7f87d8792..1bc620775 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -7,8 +7,8 @@ mod tests { WalletsConfig, }; use fuels::types::errors::Context; - use fuels_accounts::kms::AwsWallet; - use fuels_accounts::{Account, ViewOnlyAccount}; + use fuels::accounts::kms::AwsWallet; + use fuels::accounts::{Account, ViewOnlyAccount}; #[tokio::test(flavor = "multi_thread")] async fn fund_aws_wallet() -> Result<()> { @@ -34,8 +34,6 @@ mod tests { let your_kms_key_id = key.id; let provider = wallet.provider().expect("No provider found").clone(); - dbg!(key.kms_key.address()); - // ANCHOR: use_kms_wallet let wallet = AwsWallet::with_kms_key(your_kms_key_id, kms.client(), Some(provider)).await?; // ANCHOR_END: use_kms_wallet diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 2b73f4106..2f968d034 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -15,8 +15,8 @@ async-trait = { workspace = true, default-features = false } # AWS KMS client aws-config = { workspace = true, features = [ "behavior-version-latest", -] } -aws-sdk-kms = { workspace = true, features = ["default"] } +], optional = true } +aws-sdk-kms = { workspace = true, features = ["default"], optional = true } chrono = { workspace = true } cynic = { workspace = true, optional = true } elliptic-curve = { workspace = true, default-features = false } @@ -53,3 +53,4 @@ std = [ "dep:cynic", ] test-helpers = [] +kms_signer = ["dep:aws-sdk-kms", "dep:aws-config"] diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index ca1155f9f..10ef71326 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -11,7 +11,7 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; -#[cfg(feature = "std")] +#[cfg(feature = "kms_signer")] pub mod kms; #[cfg(feature = "coin-cache")] From e9cab4ef2e9e86be16ada1b54175850f5e25cfba Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 27 Feb 2025 07:48:17 +0100 Subject: [PATCH 18/34] update --- packages/fuels-accounts/src/kms/aws/client.rs | 27 ++++++++++++------- packages/fuels-accounts/src/kms/aws/wallet.rs | 2 -- packages/fuels-accounts/src/provider.rs | 8 +++--- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs index 17bbfcbbc..4874d38e5 100644 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -1,6 +1,7 @@ use aws_config::{ default_provider::credentials::DefaultCredentialsChain, BehaviorVersion, Region, SdkConfig, }; +use aws_sdk_kms::config::Credentials; use aws_sdk_kms::Client; #[derive(Clone)] @@ -8,6 +9,14 @@ pub struct AwsConfig { sdk_config: SdkConfig, } +// aws_sdk_kms::config::Credentials::new( +// "test", +// "test", +// None, +// None, +// "Static Test Credentials", +// ) + impl AwsConfig { pub async fn from_environment() -> Self { let loader = aws_config::defaults(BehaviorVersion::latest()) @@ -19,17 +28,15 @@ impl AwsConfig { } #[cfg(feature = "test-helpers")] - pub async fn for_testing(endpoint_url: String) -> Self { + pub async fn for_testing( + credentials: Credentials, + region: Region, + endpoint_url: String, + ) -> Self { let sdk_config = aws_config::defaults(BehaviorVersion::latest()) - .credentials_provider(aws_sdk_kms::config::Credentials::new( - "test", - "test", - None, - None, - "Static Test Credentials", - )) + .credentials_provider(credentials) .endpoint_url(endpoint_url) - .region(Region::new("us-east-1")) // Test region + .region(region) .load() .await; @@ -58,7 +65,7 @@ impl AwsClient { Self { client } } - pub fn inner(&self) -> &aws_sdk_kms::Client { + pub fn inner(&self) -> &Client { &self.client } } diff --git a/packages/fuels-accounts/src/kms/aws/wallet.rs b/packages/fuels-accounts/src/kms/aws/wallet.rs index a6b5c2f23..6636d2a03 100644 --- a/packages/fuels-accounts/src/kms/aws/wallet.rs +++ b/packages/fuels-accounts/src/kms/aws/wallet.rs @@ -206,8 +206,6 @@ impl AwsWallet { aws_client: &AwsClient, provider: Option, ) -> Result { - // let config = AwsConfig::from_environment().await; - // let client = AwsClient::new(&config); let kms_key = KmsKey::new(key_id.into(), aws_client).await?; Ok(Self { diff --git a/packages/fuels-accounts/src/provider.rs b/packages/fuels-accounts/src/provider.rs index f7cb4518d..a283509fd 100644 --- a/packages/fuels-accounts/src/provider.rs +++ b/packages/fuels-accounts/src/provider.rs @@ -860,6 +860,10 @@ impl DryRunner for Provider { Ok(self.estimate_gas_price(block_horizon).await?.gas_price) } + async fn consensus_parameters(&self) -> Result { + Provider::consensus_parameters(self).await + } + async fn estimate_predicates( &self, tx: &FuelTransaction, @@ -867,8 +871,4 @@ impl DryRunner for Provider { ) -> Result { Ok(self.uncached_client().estimate_predicates(tx).await?) } - - async fn consensus_parameters(&self) -> Result { - Provider::consensus_parameters(self).await - } } From 5b35ef6fa12f6675b55249da0d305dd4b3faa394 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 27 Feb 2025 07:48:28 +0100 Subject: [PATCH 19/34] update --- .github/workflows/ci.yml | 2 -- docs/src/wallets/kms-wallets.md | 2 +- e2e/Cargo.toml | 5 ++-- e2e/src/aws_kms.rs | 6 ++++- e2e/tests/aws.rs | 27 +++++-------------- packages/fuels-accounts/src/kms/aws/client.rs | 8 ------ packages/fuels/Cargo.toml | 1 + 7 files changed, 16 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aff6727ca..9b813ca61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -278,8 +278,6 @@ jobs: - name: Check for typos if: ${{ matrix.command == 'check_typos' }} uses: crate-ci/typos@v1.29.5 - with: - config: ./typos.toml publish: needs: diff --git a/docs/src/wallets/kms-wallets.md b/docs/src/wallets/kms-wallets.md index d5e9a0dbf..5bfb7269a 100644 --- a/docs/src/wallets/kms-wallets.md +++ b/docs/src/wallets/kms-wallets.md @@ -3,5 +3,5 @@ AWS Key Management Service (KMS) provides a secure way to manage cryptographic keys for your Fuel wallets. Rather than storing private keys locally, AWS KMS wallets use AWS's secure infrastructure to handle key storage and signing operations. ```rust,ignore -{{#include ../../../e2e/src/lib.rs:use_kms_wallet}} +{{#include ../../../e2e/tests/aws.rs:use_kms_wallet}} ``` diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index f984eb1d7..5a9382126 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -18,7 +18,6 @@ chrono = { workspace = true } fuel-asm = { workspace = true } # TODO: [issue](https://github.com/FuelLabs/fuels-rs/issues/1375) needs to be removed, `ScriptTransaction` and `CreateTransaction` in `fuels` use `fuel_tx::Input` but don't reexport or convert it into a `fuels` owned type fuel-tx = { workspace = true } -fuels-accounts = { workspace = true, features = ["test-helpers"] } # used in test assertions tai64 = { workspace = true } tempfile = { workspace = true } @@ -35,8 +34,7 @@ tar = { workspace = true } [dependencies] anyhow = { workspace = true } aws-sdk-kms = { workspace = true, features = ["rustls"] } -fuels = { workspace = true } -fuels-accounts = { workspace = true, features = ["std", "kms_signer", "test-helpers"] } +fuels = { workspace = true, features = ["kms_signer", "test-helpers"] } testcontainers = { workspace = true } tokio = { workspace = true, features = ["test-util"] } @@ -45,3 +43,4 @@ default = ["fuels/default", "coin-cache", ] fuel-core-lib = ["fuels/fuel-core-lib"] rocksdb = ["fuels/rocksdb"] coin-cache = ["fuels/coin-cache"] + diff --git a/e2e/src/aws_kms.rs b/e2e/src/aws_kms.rs index 7e588c0a8..896bcd3a6 100644 --- a/e2e/src/aws_kms.rs +++ b/e2e/src/aws_kms.rs @@ -1,4 +1,5 @@ use anyhow::Context; +use aws_sdk_kms::config::{Credentials, Region}; use fuels::accounts::kms::{AwsClient, AwsConfig, KmsKey}; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; @@ -47,7 +48,10 @@ impl AwsKms { let port = container.get_host_port_ipv4(4566).await?; let url = format!("http://localhost:{}", port); - let config = AwsConfig::for_testing(url.clone()).await; + let credentials = Credentials::new("test", "test", None, None, "Static Test Credentials"); + let region = Region::new("us-east-1"); + + let config = AwsConfig::for_testing(credentials, region, url.clone()).await; let client = AwsClient::new(config); Ok(AwsKmsProcess { diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index 1bc620775..d33ec47ac 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -2,25 +2,17 @@ mod tests { use anyhow::Result; use e2e::e2e_helpers::start_aws_kms; + use fuels::accounts::kms::AwsWallet; + use fuels::accounts::{Account, ViewOnlyAccount}; use fuels::prelude::{ - launch_custom_provider_and_get_wallets, AssetId, Contract, LoadConfiguration, TxPolicies, - WalletsConfig, + launch_provider_and_get_wallet, AssetId, Contract, LoadConfiguration, TxPolicies, }; use fuels::types::errors::Context; - use fuels::accounts::kms::AwsWallet; - use fuels::accounts::{Account, ViewOnlyAccount}; #[tokio::test(flavor = "multi_thread")] async fn fund_aws_wallet() -> Result<()> { let kms = start_aws_kms(false).await?; - - let mut wallets = launch_custom_provider_and_get_wallets( - WalletsConfig::new(Some(1), None, None), - None, - None, - ) - .await?; - let wallet = wallets.first_mut().expect("No wallets found"); + let wallet = launch_provider_and_get_wallet().await?; let amount = 500000000; let key = kms.create_key().await?; @@ -49,16 +41,10 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] - async fn deploy_contract() -> anyhow::Result<()> { + async fn deploy_contract() -> Result<()> { let kms = start_aws_kms(false).await?; - let mut wallets = launch_custom_provider_and_get_wallets( - WalletsConfig::new(Some(1), None, None), - None, - None, - ) - .await?; - let wallet = wallets.first_mut().expect("No wallets found"); + let wallet = launch_provider_and_get_wallet().await?; let amount = 500000000; let key = kms.create_key().await?; @@ -96,6 +82,7 @@ mod tests { .first() .expect("No coins found") .amount; + assert_eq!(founded_coins, 499999999); Ok(()) diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs index 4874d38e5..bcd549b07 100644 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -9,14 +9,6 @@ pub struct AwsConfig { sdk_config: SdkConfig, } -// aws_sdk_kms::config::Credentials::new( -// "test", -// "test", -// None, -// None, -// "Static Test Credentials", -// ) - impl AwsConfig { pub async fn from_environment() -> Self { let loader = aws_config::defaults(BehaviorVersion::latest()) diff --git a/packages/fuels/Cargo.toml b/packages/fuels/Cargo.toml index 22df12e6b..2e27f617f 100644 --- a/packages/fuels/Cargo.toml +++ b/packages/fuels/Cargo.toml @@ -41,3 +41,4 @@ std = [ ] fuel-core-lib = ["fuels-test-helpers?/fuel-core-lib"] rocksdb = ["fuels-test-helpers?/rocksdb"] +kms_signer = ["fuels-accounts/kms_signer"] \ No newline at end of file From 268a4292238c133d43e7e6ed406dc567c0b90098 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 27 Feb 2025 12:54:46 +0100 Subject: [PATCH 20/34] add fix --- packages/fuels-accounts/src/kms/aws/wallet.rs | 131 ++++++++++-------- 1 file changed, 76 insertions(+), 55 deletions(-) diff --git a/packages/fuels-accounts/src/kms/aws/wallet.rs b/packages/fuels-accounts/src/kms/aws/wallet.rs index 6636d2a03..57673e714 100644 --- a/packages/fuels-accounts/src/kms/aws/wallet.rs +++ b/packages/fuels-accounts/src/kms/aws/wallet.rs @@ -1,3 +1,4 @@ +use crate::accounts_utils::try_provider_error; use crate::kms::aws::client::AwsClient; use crate::provider::Provider; use crate::wallet::Wallet; @@ -24,19 +25,24 @@ use k256::{ PublicKey as K256PublicKey, }; +/// Error prefix for AWS KMS related operations const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; +/// Expected key specification for AWS KMS keys +const EXPECTED_KEY_SPEC: KeySpec = KeySpec::EccSecgP256K1; +/// A wallet implementation that uses AWS KMS for signing #[derive(Clone, Debug)] pub struct AwsWallet { view_account: Wallet, kms_key: KmsKey, } +/// Represents an AWS KMS key with Fuel-compatible address #[derive(Clone, Debug)] pub struct KmsKey { key_id: String, client: AwsClient, - public_key: Vec, + public_key_der: Vec, fuel_address: Bech32Address, } @@ -44,84 +50,88 @@ impl KmsKey { pub fn key_id(&self) -> &String { &self.key_id } + pub fn public_key(&self) -> &Vec { - &self.public_key + &self.public_key_der } + pub fn fuel_address(&self) -> &Bech32Address { &self.fuel_address } -} -impl KmsKey { + /// Creates a new KmsKey from an AWS KMS key ID pub async fn new(key_id: String, client: &AwsClient) -> Result { - Self::validate_key_type(client, &key_id).await?; - let public_key = Self::fetch_public_key(client, &key_id).await?; + Self::validate_key_spec(client, &key_id).await?; + let public_key = Self::retrieve_public_key(client, &key_id).await?; let fuel_address = Self::derive_fuel_address(&public_key)?; Ok(Self { key_id, client: client.clone(), - public_key, + public_key_der: public_key, fuel_address, }) } - async fn validate_key_type(client: &AwsClient, key_id: &str) -> Result<()> { - let key_spec = client + /// Validates that the KMS key is of the expected type + async fn validate_key_spec(client: &AwsClient, key_id: &str) -> Result<()> { + let response = client .inner() .get_public_key() .key_id(key_id) .send() .await - .map_err(|e| Error::Other(format!("{}: {}", AWS_KMS_ERROR_PREFIX, e)))? - .key_spec; + .map_err(format_kms_error)?; + + let key_spec = response.key_spec; match key_spec { - Some(KeySpec::EccSecgP256K1) => Ok(()), + Some(EXPECTED_KEY_SPEC) => Ok(()), other => Err(Error::Other(format!( - "{}: Invalid key type {:?}, expected ECC_SECG_P256K1", - AWS_KMS_ERROR_PREFIX, other + "{AWS_KMS_ERROR_PREFIX}: Invalid key type {other:?}, expected {EXPECTED_KEY_SPEC:?}" ))), } } - async fn fetch_public_key(client: &AwsClient, key_id: &str) -> Result> { + /// Retrieves the public key from AWS KMS + async fn retrieve_public_key(client: &AwsClient, key_id: &str) -> Result> { let response = client .inner() .get_public_key() .key_id(key_id) .send() .await - .map_err(|e| Error::Other(format!("{}: {}", AWS_KMS_ERROR_PREFIX, e)))?; + .map_err(format_kms_error)?; response .public_key() .map(|blob| blob.as_ref().to_vec()) .ok_or_else(|| { - Error::Other(format!( - "{}: Empty public key response", - AWS_KMS_ERROR_PREFIX - )) + Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: Empty public key response")) }) } + /// Derives a Fuel address from a public key in DER format fn derive_fuel_address(public_key: &[u8]) -> Result { let k256_key = K256PublicKey::from_public_key_der(public_key) - .map_err(|_| Error::Other(format!("{}: Invalid DER encoding", AWS_KMS_ERROR_PREFIX)))?; + .map_err(|_| Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: Invalid DER encoding")))?; let fuel_public_key = PublicKey::from(k256_key); Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_public_key.hash())) } + /// Signs a message using the AWS KMS key async fn sign_message(&self, message: Message) -> Result { let signature_der = self.request_kms_signature(message).await?; let (sig, recovery_id) = self.normalize_signature(&signature_der, message)?; - Ok(self.format_fuel_signature(sig, recovery_id)) + Ok(self.convert_to_fuel_signature(sig, recovery_id)) } + /// Requests a signature from AWS KMS async fn request_kms_signature(&self, message: Message) -> Result> { - self.client + let response = self + .client .inner() .sign() .key_id(&self.key_id) @@ -130,77 +140,84 @@ impl KmsKey { .message(Blob::new(message.as_ref().to_vec())) .send() .await - .map_err(|e| Error::Other(format!("{}: Signing failed - {}", AWS_KMS_ERROR_PREFIX, e)))? + .map_err(|err| { + Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: Signing failed - {err}")) + })?; + + response .signature .map(|blob| blob.into_inner()) .ok_or_else(|| { - Error::Other(format!( - "{}: Empty signature response", - AWS_KMS_ERROR_PREFIX - )) + Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: Empty signature response")) }) } + /// Normalizes a DER signature and determines the recovery ID fn normalize_signature( &self, signature_der: &[u8], message: Message, ) -> Result<(K256Signature, RecoveryId)> { - let mut sig = K256Signature::from_der(signature_der).map_err(|_| { - Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) - })?; + let signature = K256Signature::from_der(signature_der) + .map_err(|_| Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: Invalid DER signature")))?; - sig = sig.normalize_s().unwrap_or(sig); + // Ensure the signature is in normalized form (low-S value) + let normalized_sig = signature.normalize_s().unwrap_or(signature); + let recovery_id = self.determine_recovery_id(&normalized_sig, message)?; - let recovery_id = self.determine_recovery_id(&sig, message)?; - Ok((sig, recovery_id)) + Ok((normalized_sig, recovery_id)) } + /// Determines the correct recovery ID for the signature fn determine_recovery_id(&self, sig: &K256Signature, message: Message) -> Result { - let recid1 = RecoveryId::new(false, false); - let recid2 = RecoveryId::new(true, false); + let recid_even = RecoveryId::new(false, false); + let recid_odd = RecoveryId::new(true, false); - let correct_public_key = K256PublicKey::from_public_key_der(&self.public_key) + // Get the expected public key + let expected_pubkey = K256PublicKey::from_public_key_der(&self.public_key_der) .map_err(|_| { - Error::Other(format!( - "{}: Invalid cached public key", - AWS_KMS_ERROR_PREFIX - )) + Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: Invalid cached public key")) })? .into(); - let rec1 = VerifyingKey::recover_from_prehash(&*message, sig, recid1); - let rec2 = VerifyingKey::recover_from_prehash(&*message, sig, recid2); - - if rec1.map(|r| r == correct_public_key).unwrap_or(false) { - Ok(recid1) - } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { - Ok(recid2) + // Try recovery with each recovery ID + let recovered_even = VerifyingKey::recover_from_prehash(&*message, sig, recid_even); + let recovered_odd = VerifyingKey::recover_from_prehash(&*message, sig, recid_odd); + + if recovered_even + .map(|r| r == expected_pubkey) + .unwrap_or(false) + { + Ok(recid_even) + } else if recovered_odd.map(|r| r == expected_pubkey).unwrap_or(false) { + Ok(recid_odd) } else { Err(Error::Other(format!( - "{}: Invalid signature (reduced-x form coordinate)", - AWS_KMS_ERROR_PREFIX + "{AWS_KMS_ERROR_PREFIX}: Invalid signature (could not recover correct public key)" ))) } } - fn format_fuel_signature( + /// Converts a k256 signature to a Fuel signature format + fn convert_to_fuel_signature( &self, signature: K256Signature, recovery_id: RecoveryId, ) -> Signature { let recovery_byte = recovery_id.is_y_odd() as u8; let mut bytes: [u8; 64] = signature.to_bytes().into(); - bytes[63] = (recovery_byte << 7) | (bytes[63] & 0x7F); + bytes[32] = (recovery_byte << 7) | (bytes[32] & 0x7F); Signature::from_bytes(bytes) } + /// Returns the Fuel address associated with this key pub fn address(&self) -> &Bech32Address { &self.fuel_address } } impl AwsWallet { + /// Creates a new AwsWallet with the given KMS key ID pub async fn with_kms_key( key_id: impl Into, aws_client: &AwsClient, @@ -214,10 +231,12 @@ impl AwsWallet { }) } + /// Returns the Fuel address associated with this wallet pub fn address(&self) -> &Bech32Address { &self.kms_key.fuel_address } + /// Returns the provider associated with this wallet, if any pub fn provider(&self) -> Option<&Provider> { self.view_account.provider() } @@ -241,9 +260,7 @@ impl ViewOnlyAccount for AwsWallet { } fn try_provider(&self) -> Result<&Provider> { - self.provider().ok_or_else(|| { - Error::Other("Provider required - use `.with_provider()` when creating wallet".into()) - }) + self.provider().ok_or_else(try_provider_error) } async fn get_asset_inputs_for_amount( @@ -265,3 +282,7 @@ impl Account for AwsWallet { Ok(()) } } + +fn format_kms_error(err: impl std::fmt::Display) -> Error { + Error::Other(format!("{AWS_KMS_ERROR_PREFIX}: {err}")) +} From 0d177fd8f62d8564fa15e500966a618f43f2a792 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 27 Feb 2025 13:15:40 +0100 Subject: [PATCH 21/34] add fix --- packages/fuels-accounts/src/kms/aws/wallet.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/fuels-accounts/src/kms/aws/wallet.rs b/packages/fuels-accounts/src/kms/aws/wallet.rs index 57673e714..bf2ceb3d8 100644 --- a/packages/fuels-accounts/src/kms/aws/wallet.rs +++ b/packages/fuels-accounts/src/kms/aws/wallet.rs @@ -25,9 +25,7 @@ use k256::{ PublicKey as K256PublicKey, }; -/// Error prefix for AWS KMS related operations const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; -/// Expected key specification for AWS KMS keys const EXPECTED_KEY_SPEC: KeySpec = KeySpec::EccSecgP256K1; /// A wallet implementation that uses AWS KMS for signing @@ -37,7 +35,6 @@ pub struct AwsWallet { kms_key: KmsKey, } -/// Represents an AWS KMS key with Fuel-compatible address #[derive(Clone, Debug)] pub struct KmsKey { key_id: String, @@ -210,7 +207,6 @@ impl KmsKey { Signature::from_bytes(bytes) } - /// Returns the Fuel address associated with this key pub fn address(&self) -> &Bech32Address { &self.fuel_address } From 162ddec358e800fe4b95020905e2e1bc881f7f37 Mon Sep 17 00:00:00 2001 From: Emir Date: Thu, 27 Feb 2025 14:08:59 +0100 Subject: [PATCH 22/34] Update e2e/Cargo.toml Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> --- e2e/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 5a9382126..69093f367 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -26,7 +26,6 @@ tempfile = { workspace = true } anyhow = { workspace = true, features = ["std"] } flate2 = { workspace = true, features = ["zlib"] } fuels-accounts = { workspace = true, features = ["std"] } - reqwest = { workspace = true, features = ["blocking", "default-tls"] } semver = { workspace = true } tar = { workspace = true } From a31ba566d1f9bf49e233c65fa40f06308bf5b945 Mon Sep 17 00:00:00 2001 From: Emir Date: Thu, 27 Feb 2025 14:09:14 +0100 Subject: [PATCH 23/34] Update e2e/Cargo.toml Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> --- e2e/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 69093f367..f1ee14376 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -42,4 +42,3 @@ default = ["fuels/default", "coin-cache", ] fuel-core-lib = ["fuels/fuel-core-lib"] rocksdb = ["fuels/rocksdb"] coin-cache = ["fuels/coin-cache"] - From 4943694f7b826d28dea97424e24cad7f0036296d Mon Sep 17 00:00:00 2001 From: Emir Date: Thu, 27 Feb 2025 14:11:44 +0100 Subject: [PATCH 24/34] Update e2e/tests/aws.rs Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> --- e2e/tests/aws.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index d33ec47ac..18c0af32f 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -36,7 +36,7 @@ mod tests { .first() .expect("No coins found") .amount; - assert_eq!(founded_coins, 500000000); + assert_eq!(founded_coins, amount); Ok(()) } From 9f1516916d90394bfc537ced95efd6520b20ff93 Mon Sep 17 00:00:00 2001 From: Emir Date: Thu, 27 Feb 2025 14:11:52 +0100 Subject: [PATCH 25/34] Update packages/fuels-accounts/Cargo.toml Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> --- packages/fuels-accounts/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 2f968d034..e6bece228 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -11,8 +11,6 @@ description = "Fuel Rust SDK accounts." [dependencies] async-trait = { workspace = true, default-features = false } - -# AWS KMS client aws-config = { workspace = true, features = [ "behavior-version-latest", ], optional = true } From da68a3b9761ddbd5def73deab1ed5aeccaef349e Mon Sep 17 00:00:00 2001 From: Emir Date: Thu, 27 Feb 2025 14:12:11 +0100 Subject: [PATCH 26/34] Update e2e/Cargo.toml Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> --- e2e/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index f1ee14376..e8cc3c155 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -38,7 +38,7 @@ testcontainers = { workspace = true } tokio = { workspace = true, features = ["test-util"] } [features] -default = ["fuels/default", "coin-cache", ] +default = ["fuels/default", "coin-cache"] fuel-core-lib = ["fuels/fuel-core-lib"] rocksdb = ["fuels/rocksdb"] coin-cache = ["fuels/coin-cache"] From 4b6e5ce62080d2b4a8c729800673290940f0508e Mon Sep 17 00:00:00 2001 From: Emir Date: Thu, 27 Feb 2025 14:16:00 +0100 Subject: [PATCH 27/34] Update e2e/tests/aws.rs Co-authored-by: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> --- e2e/tests/aws.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index 18c0af32f..0eb966d95 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -67,7 +67,7 @@ mod tests { .first() .expect("No coins found") .amount; - assert_eq!(founded_coins, 500000000); + assert_eq!(founded_coins, amount); Contract::load_from( "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", From dc41cb2d807b37cea706fd0928a1c91c1325e929 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 27 Feb 2025 14:16:28 +0100 Subject: [PATCH 28/34] add --- Cargo.toml | 1 - e2e/Cargo.toml | 2 +- e2e/tests/aws.rs | 2 +- packages/fuels-accounts/Cargo.toml | 2 +- packages/fuels-accounts/src/lib.rs | 2 +- packages/fuels/Cargo.toml | 2 +- scripts/change-log/src/get_full_changelog.rs | 0 7 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 scripts/change-log/src/get_full_changelog.rs diff --git a/Cargo.toml b/Cargo.toml index 55f946626..c5d5a2d35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,6 @@ octocrab = { version = "0.43", default-features = false } dotenv = { version = "0.15", default-features = false } toml = { version = "0.8", default-features = false } mockall = { version = "0.13", default-features = false } - aws-config = { version = "1", default-features = false } aws-sdk-kms = { version = "1", default-features = false } testcontainers = { version = "0.23", default-features = false } diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index f1ee14376..c563ac0f7 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -33,7 +33,7 @@ tar = { workspace = true } [dependencies] anyhow = { workspace = true } aws-sdk-kms = { workspace = true, features = ["rustls"] } -fuels = { workspace = true, features = ["kms_signer", "test-helpers"] } +fuels = { workspace = true, features = ["kms-signer", "test-helpers"] } testcontainers = { workspace = true } tokio = { workspace = true, features = ["test-util"] } diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index d33ec47ac..18c0af32f 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -36,7 +36,7 @@ mod tests { .first() .expect("No coins found") .amount; - assert_eq!(founded_coins, 500000000); + assert_eq!(founded_coins, amount); Ok(()) } diff --git a/packages/fuels-accounts/Cargo.toml b/packages/fuels-accounts/Cargo.toml index 2f968d034..96e942714 100644 --- a/packages/fuels-accounts/Cargo.toml +++ b/packages/fuels-accounts/Cargo.toml @@ -53,4 +53,4 @@ std = [ "dep:cynic", ] test-helpers = [] -kms_signer = ["dep:aws-sdk-kms", "dep:aws-config"] +kms-signer = ["dep:aws-sdk-kms", "dep:aws-config"] diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 10ef71326..6d05ad423 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -11,7 +11,7 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; -#[cfg(feature = "kms_signer")] +#[cfg(feature = "kms-signer")] pub mod kms; #[cfg(feature = "coin-cache")] diff --git a/packages/fuels/Cargo.toml b/packages/fuels/Cargo.toml index 2e27f617f..6c84c2844 100644 --- a/packages/fuels/Cargo.toml +++ b/packages/fuels/Cargo.toml @@ -41,4 +41,4 @@ std = [ ] fuel-core-lib = ["fuels-test-helpers?/fuel-core-lib"] rocksdb = ["fuels-test-helpers?/rocksdb"] -kms_signer = ["fuels-accounts/kms_signer"] \ No newline at end of file +kms-signer = ["fuels-accounts/kms-signer"] diff --git a/scripts/change-log/src/get_full_changelog.rs b/scripts/change-log/src/get_full_changelog.rs deleted file mode 100644 index e69de29bb..000000000 From d40dbe525c479456a9f990ff401f4fa3dca4ad72 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Thu, 27 Feb 2025 14:25:14 +0100 Subject: [PATCH 29/34] fix --- packages/fuels-accounts/src/kms/aws/client.rs | 4 ++-- packages/fuels-accounts/src/kms/aws/wallet.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs index bcd549b07..02256f1c5 100644 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -1,7 +1,7 @@ -use aws_config::{ +pub use aws_config::{ default_provider::credentials::DefaultCredentialsChain, BehaviorVersion, Region, SdkConfig, }; -use aws_sdk_kms::config::Credentials; +pub use aws_sdk_kms::config::Credentials; use aws_sdk_kms::Client; #[derive(Clone)] diff --git a/packages/fuels-accounts/src/kms/aws/wallet.rs b/packages/fuels-accounts/src/kms/aws/wallet.rs index bf2ceb3d8..09220eb89 100644 --- a/packages/fuels-accounts/src/kms/aws/wallet.rs +++ b/packages/fuels-accounts/src/kms/aws/wallet.rs @@ -1,5 +1,5 @@ use crate::accounts_utils::try_provider_error; -use crate::kms::aws::client::AwsClient; +pub use crate::kms::aws::client::AwsClient; use crate::provider::Provider; use crate::wallet::Wallet; use crate::{Account, ViewOnlyAccount}; From 0b8a075bc5363e17a79cddebc05ea2140beacb33 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Fri, 28 Feb 2025 13:45:13 +0100 Subject: [PATCH 30/34] fix --- e2e/src/aws_kms.rs | 15 ++++++++++----- e2e/src/e2e_helpers.rs | 3 ++- e2e/tests/aws.rs | 18 ++---------------- packages/fuels-accounts/src/kms/aws/client.rs | 9 --------- 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/e2e/src/aws_kms.rs b/e2e/src/aws_kms.rs index 896bcd3a6..a58e848fb 100644 --- a/e2e/src/aws_kms.rs +++ b/e2e/src/aws_kms.rs @@ -1,6 +1,7 @@ -use anyhow::Context; -use aws_sdk_kms::config::{Credentials, Region}; -use fuels::accounts::kms::{AwsClient, AwsConfig, KmsKey}; +use fuels::accounts::kms::{AwsClient, AwsConfig, Credentials, KmsKey, Region}; +use fuels::prelude::Error; +use fuels::types::errors::Context; +use fuels::types::errors::Result; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; @@ -35,17 +36,21 @@ impl AwsKms { self } - pub async fn start(self) -> anyhow::Result { + pub async fn start(self) -> Result { let container = AwsKmsImage .start() .await + .map_err(|e| Error::Other(e.to_string())) .with_context(|| "Failed to start KMS container")?; if self.show_logs { spawn_log_printer(&container); } - let port = container.get_host_port_ipv4(4566).await?; + let port = container + .get_host_port_ipv4(4566) + .await + .map_err(|e| Error::Other(e.to_string()))?; let url = format!("http://localhost:{}", port); let credentials = Credentials::new("test", "test", None, None, "Static Test Credentials"); diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs index eba14ece4..9e4415734 100644 --- a/e2e/src/e2e_helpers.rs +++ b/e2e/src/e2e_helpers.rs @@ -1,5 +1,6 @@ use crate::aws_kms::{AwsKms, AwsKmsProcess}; +use fuels::types::errors::Result; -pub async fn start_aws_kms(logs: bool) -> anyhow::Result { +pub async fn start_aws_kms(logs: bool) -> Result { AwsKms::default().with_show_logs(logs).start().await } diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index 0eb966d95..f7583d833 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -30,13 +30,8 @@ mod tests { let wallet = AwsWallet::with_kms_key(your_kms_key_id, kms.client(), Some(provider)).await?; // ANCHOR_END: use_kms_wallet - let founded_coins = wallet - .get_coins(AssetId::zeroed()) - .await? - .first() - .expect("No coins found") - .amount; - assert_eq!(founded_coins, amount); + let total_base_balance = wallet.get_asset_balance(&AssetId::zeroed()).await?; + assert_eq!(total_base_balance, amount); Ok(()) } @@ -76,15 +71,6 @@ mod tests { .deploy(aws_wallet, TxPolicies::default()) .await?; - let founded_coins = wallet - .get_coins(AssetId::zeroed()) - .await? - .first() - .expect("No coins found") - .amount; - - assert_eq!(founded_coins, 499999999); - Ok(()) } } diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs index 02256f1c5..e87f7304f 100644 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -10,15 +10,6 @@ pub struct AwsConfig { } impl AwsConfig { - pub async fn from_environment() -> Self { - let loader = aws_config::defaults(BehaviorVersion::latest()) - .credentials_provider(DefaultCredentialsChain::builder().build().await); - - Self { - sdk_config: loader.load().await, - } - } - #[cfg(feature = "test-helpers")] pub async fn for_testing( credentials: Credentials, From 5282904ba425ce2b42e76f51e8bb5e7d06c2325c Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Fri, 28 Feb 2025 14:17:09 +0100 Subject: [PATCH 31/34] fix --- e2e/src/aws_kms.rs | 10 ++++-- packages/fuels-accounts/src/kms/aws/client.rs | 36 ++----------------- 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/e2e/src/aws_kms.rs b/e2e/src/aws_kms.rs index a58e848fb..adfccb8bd 100644 --- a/e2e/src/aws_kms.rs +++ b/e2e/src/aws_kms.rs @@ -1,4 +1,4 @@ -use fuels::accounts::kms::{AwsClient, AwsConfig, Credentials, KmsKey, Region}; +use fuels::accounts::kms::{defaults, AwsClient, BehaviorVersion, Credentials, KmsKey, Region}; use fuels::prelude::Error; use fuels::types::errors::Context; use fuels::types::errors::Result; @@ -56,7 +56,13 @@ impl AwsKms { let credentials = Credentials::new("test", "test", None, None, "Static Test Credentials"); let region = Region::new("us-east-1"); - let config = AwsConfig::for_testing(credentials, region, url.clone()).await; + let config = defaults(BehaviorVersion::latest()) + .credentials_provider(credentials) + .endpoint_url(url.clone()) + .region(region) + .load() + .await; + let client = AwsClient::new(config); Ok(AwsKmsProcess { diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs index e87f7304f..9ed41253d 100644 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -1,48 +1,16 @@ pub use aws_config::{ - default_provider::credentials::DefaultCredentialsChain, BehaviorVersion, Region, SdkConfig, + default_provider::credentials::DefaultCredentialsChain, BehaviorVersion, Region, SdkConfig, defaults }; pub use aws_sdk_kms::config::Credentials; use aws_sdk_kms::Client; -#[derive(Clone)] -pub struct AwsConfig { - sdk_config: SdkConfig, -} - -impl AwsConfig { - #[cfg(feature = "test-helpers")] - pub async fn for_testing( - credentials: Credentials, - region: Region, - endpoint_url: String, - ) -> Self { - let sdk_config = aws_config::defaults(BehaviorVersion::latest()) - .credentials_provider(credentials) - .endpoint_url(endpoint_url) - .region(region) - .load() - .await; - - Self { sdk_config } - } - - pub fn endpoint_url(&self) -> Option<&str> { - self.sdk_config.endpoint_url() - } - - pub fn region(&self) -> Option<&Region> { - self.sdk_config.region() - } -} - #[derive(Clone, Debug)] pub struct AwsClient { client: Client, } impl AwsClient { - pub fn new(config: AwsConfig) -> Self { - let config = config.sdk_config; + pub fn new(config: SdkConfig) -> Self { let client = Client::new(&config); Self { client } From 6bab0fd7d1fb9bdda4c9cc9f70e25b2cc3eee8a9 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Fri, 28 Feb 2025 14:21:09 +0100 Subject: [PATCH 32/34] fix --- packages/fuels-accounts/src/kms/aws/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs index 9ed41253d..8a0025e30 100644 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ b/packages/fuels-accounts/src/kms/aws/client.rs @@ -1,5 +1,6 @@ pub use aws_config::{ - default_provider::credentials::DefaultCredentialsChain, BehaviorVersion, Region, SdkConfig, defaults + default_provider::credentials::DefaultCredentialsChain, defaults, BehaviorVersion, Region, + SdkConfig, }; pub use aws_sdk_kms::config::Credentials; use aws_sdk_kms::Client; From 57da2fccc3968e93fdcf291c3279c9c2efcf5131 Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 3 Mar 2025 15:53:32 +0100 Subject: [PATCH 33/34] fix --- e2e/Cargo.toml | 1 - e2e/src/aws_kms.rs | 21 +++++++++++------ packages/fuels-accounts/src/kms/aws.rs | 6 ++--- packages/fuels-accounts/src/kms/aws/client.rs | 23 ------------------- packages/fuels-accounts/src/kms/aws/wallet.rs | 15 +++++------- 5 files changed, 23 insertions(+), 43 deletions(-) delete mode 100644 packages/fuels-accounts/src/kms/aws/client.rs diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index 04d46911c..10fb0a190 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -32,7 +32,6 @@ tar = { workspace = true } [dependencies] anyhow = { workspace = true } -aws-sdk-kms = { workspace = true, features = ["rustls"] } fuels = { workspace = true, features = ["kms-signer", "test-helpers"] } testcontainers = { workspace = true } tokio = { workspace = true, features = ["test-util"] } diff --git a/e2e/src/aws_kms.rs b/e2e/src/aws_kms.rs index adfccb8bd..62a77ab05 100644 --- a/e2e/src/aws_kms.rs +++ b/e2e/src/aws_kms.rs @@ -1,4 +1,12 @@ -use fuels::accounts::kms::{defaults, AwsClient, BehaviorVersion, Credentials, KmsKey, Region}; +use fuels::accounts::kms::{ + aws_config::{defaults, BehaviorVersion, Region}, + aws_sdk_kms::{ + config::Credentials, + types::{KeySpec, KeyUsageType}, + Client, + }, + KmsKey, +}; use fuels::prelude::Error; use fuels::types::errors::Context; use fuels::types::errors::Result; @@ -63,7 +71,7 @@ impl AwsKms { .load() .await; - let client = AwsClient::new(config); + let client = Client::new(&config); Ok(AwsKmsProcess { _container: container, @@ -114,7 +122,7 @@ fn spawn_log_printer(container: &testcontainers::ContainerAsync) { pub struct AwsKmsProcess { _container: testcontainers::ContainerAsync, - client: AwsClient, + client: Client, url: String, } @@ -122,10 +130,9 @@ impl AwsKmsProcess { pub async fn create_key(&self) -> anyhow::Result { let response = self .client - .inner() .create_key() - .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) - .key_spec(aws_sdk_kms::types::KeySpec::EccSecgP256K1) + .key_usage(KeyUsageType::SignVerify) + .key_spec(KeySpec::EccSecgP256K1) .send() .await?; @@ -143,7 +150,7 @@ impl AwsKmsProcess { }) } - pub fn client(&self) -> &AwsClient { + pub fn client(&self) -> &Client { &self.client } diff --git a/packages/fuels-accounts/src/kms/aws.rs b/packages/fuels-accounts/src/kms/aws.rs index f0b281aa9..3f44d9cb6 100644 --- a/packages/fuels-accounts/src/kms/aws.rs +++ b/packages/fuels-accounts/src/kms/aws.rs @@ -1,5 +1,5 @@ -mod client; mod wallet; - -pub use client::*; pub use wallet::*; + +pub use aws_config; +pub use aws_sdk_kms; diff --git a/packages/fuels-accounts/src/kms/aws/client.rs b/packages/fuels-accounts/src/kms/aws/client.rs deleted file mode 100644 index 8a0025e30..000000000 --- a/packages/fuels-accounts/src/kms/aws/client.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub use aws_config::{ - default_provider::credentials::DefaultCredentialsChain, defaults, BehaviorVersion, Region, - SdkConfig, -}; -pub use aws_sdk_kms::config::Credentials; -use aws_sdk_kms::Client; - -#[derive(Clone, Debug)] -pub struct AwsClient { - client: Client, -} - -impl AwsClient { - pub fn new(config: SdkConfig) -> Self { - let client = Client::new(&config); - - Self { client } - } - - pub fn inner(&self) -> &Client { - &self.client - } -} diff --git a/packages/fuels-accounts/src/kms/aws/wallet.rs b/packages/fuels-accounts/src/kms/aws/wallet.rs index 09220eb89..90d9992d8 100644 --- a/packages/fuels-accounts/src/kms/aws/wallet.rs +++ b/packages/fuels-accounts/src/kms/aws/wallet.rs @@ -1,11 +1,11 @@ use crate::accounts_utils::try_provider_error; -pub use crate::kms::aws::client::AwsClient; use crate::provider::Provider; use crate::wallet::Wallet; use crate::{Account, ViewOnlyAccount}; use aws_sdk_kms::{ primitives::Blob, types::{KeySpec, MessageType, SigningAlgorithmSpec}, + Client, }; use fuel_crypto::{Message, PublicKey, Signature}; use fuel_types::AssetId; @@ -38,7 +38,7 @@ pub struct AwsWallet { #[derive(Clone, Debug)] pub struct KmsKey { key_id: String, - client: AwsClient, + client: Client, public_key_der: Vec, fuel_address: Bech32Address, } @@ -57,7 +57,7 @@ impl KmsKey { } /// Creates a new KmsKey from an AWS KMS key ID - pub async fn new(key_id: String, client: &AwsClient) -> Result { + pub async fn new(key_id: String, client: &Client) -> Result { Self::validate_key_spec(client, &key_id).await?; let public_key = Self::retrieve_public_key(client, &key_id).await?; let fuel_address = Self::derive_fuel_address(&public_key)?; @@ -71,9 +71,8 @@ impl KmsKey { } /// Validates that the KMS key is of the expected type - async fn validate_key_spec(client: &AwsClient, key_id: &str) -> Result<()> { + async fn validate_key_spec(client: &Client, key_id: &str) -> Result<()> { let response = client - .inner() .get_public_key() .key_id(key_id) .send() @@ -91,9 +90,8 @@ impl KmsKey { } /// Retrieves the public key from AWS KMS - async fn retrieve_public_key(client: &AwsClient, key_id: &str) -> Result> { + async fn retrieve_public_key(client: &Client, key_id: &str) -> Result> { let response = client - .inner() .get_public_key() .key_id(key_id) .send() @@ -129,7 +127,6 @@ impl KmsKey { async fn request_kms_signature(&self, message: Message) -> Result> { let response = self .client - .inner() .sign() .key_id(&self.key_id) .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) @@ -216,7 +213,7 @@ impl AwsWallet { /// Creates a new AwsWallet with the given KMS key ID pub async fn with_kms_key( key_id: impl Into, - aws_client: &AwsClient, + aws_client: &Client, provider: Option, ) -> Result { let kms_key = KmsKey::new(key_id.into(), aws_client).await?; From a8c86ccbeff56a4a41cac703311ab6fb3baadf4f Mon Sep 17 00:00:00 2001 From: Salka1988 Date: Mon, 3 Mar 2025 16:31:33 +0100 Subject: [PATCH 34/34] fix --- e2e/tests/aws.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/e2e/tests/aws.rs b/e2e/tests/aws.rs index f7583d833..683c3bf10 100644 --- a/e2e/tests/aws.rs +++ b/e2e/tests/aws.rs @@ -56,14 +56,6 @@ mod tests { let aws_wallet = &AwsWallet::with_kms_key(your_kms_key_id, kms.client(), Some(provider)).await?; - let founded_coins = aws_wallet - .get_coins(AssetId::zeroed()) - .await? - .first() - .expect("No coins found") - .amount; - assert_eq!(founded_coins, amount); - Contract::load_from( "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", LoadConfiguration::default(),