use std::{collections::HashMap, fmt::Display};
use async_trait::async_trait;
use fuel_core_client::client::pagination::{PaginatedResult, PaginationRequest};
#[doc(no_inline)]
pub use fuel_crypto;
use fuel_crypto::Signature;
use fuel_tx::{Output, Receipt, TxId, TxPointer, UtxoId};
use fuel_types::{AssetId, Bytes32, ContractId, MessageId};
use fuels_core::{
constants::BASE_ASSET_ID,
types::{
bech32::{Bech32Address, Bech32ContractId},
coin::Coin,
coin_type::CoinType,
errors::{Error, Result},
input::Input,
message::Message,
transaction::{Transaction, TxParameters},
transaction_builders::{ScriptTransactionBuilder, TransactionBuilder},
transaction_response::TransactionResponse,
},
};
use provider::ResourceFilter;
use crate::{accounts_utils::extract_message_id, provider::Provider};
mod accounts_utils;
pub mod predicate;
pub mod provider;
pub mod wallet;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Signer: std::fmt::Debug + Send + Sync {
type Error: std::error::Error + Send + Sync;
async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
&self,
message: S,
) -> std::result::Result<Signature, Self::Error>;
fn sign_transaction(
&self,
message: &mut impl Transaction,
) -> std::result::Result<Signature, Self::Error>;
}
#[derive(Debug)]
pub struct AccountError(String);
impl AccountError {
pub fn no_provider() -> Self {
Self("No provider was setup: make sure to set_provider in your account!".to_string())
}
}
impl Display for AccountError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for AccountError {}
impl From<AccountError> for Error {
fn from(e: AccountError) -> Self {
Error::AccountError(e.0)
}
}
type AccountResult<T> = std::result::Result<T, AccountError>;
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ViewOnlyAccount: std::fmt::Debug + Send + Sync + Clone {
fn address(&self) -> &Bech32Address;
fn try_provider(&self) -> AccountResult<&Provider>;
async fn get_transactions(
&self,
request: PaginationRequest<String>,
) -> Result<PaginatedResult<TransactionResponse, String>> {
Ok(self
.try_provider()?
.get_transactions_by_owner(self.address(), request)
.await?)
}
async fn get_coins(&self, asset_id: AssetId) -> Result<Vec<Coin>> {
Ok(self
.try_provider()?
.get_coins(self.address(), asset_id)
.await?)
}
async fn get_asset_balance(&self, asset_id: &AssetId) -> Result<u64> {
self.try_provider()?
.get_asset_balance(self.address(), *asset_id)
.await
.map_err(Into::into)
}
async fn get_messages(&self) -> Result<Vec<Message>> {
Ok(self.try_provider()?.get_messages(self.address()).await?)
}
async fn get_balances(&self) -> Result<HashMap<String, u64>> {
self.try_provider()?
.get_balances(self.address())
.await
.map_err(Into::into)
}
async fn get_spendable_resources(
&self,
asset_id: AssetId,
amount: u64,
) -> Result<Vec<CoinType>> {
let filter = ResourceFilter {
from: self.address().clone(),
asset_id,
amount,
..Default::default()
};
self.try_provider()?
.get_spendable_resources(filter)
.await
.map_err(Into::into)
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Account: ViewOnlyAccount {
async fn get_asset_inputs_for_amount(
&self,
asset_id: AssetId,
amount: u64,
witness_index: Option<u8>,
) -> Result<Vec<Input>>;
fn get_asset_outputs_for_amount(
&self,
to: &Bech32Address,
asset_id: AssetId,
amount: u64,
) -> Vec<Output> {
vec![
Output::coin(to.into(), amount, asset_id),
Output::change(self.address().into(), 0, asset_id),
]
}
async fn add_fee_resources<Tb: TransactionBuilder>(
&self,
tb: Tb,
previous_base_amount: u64,
witness_index: Option<u8>,
) -> Result<Tb::TxType>;
async fn transfer(
&self,
to: &Bech32Address,
amount: u64,
asset_id: AssetId,
tx_parameters: TxParameters,
) -> Result<(TxId, Vec<Receipt>)> {
let inputs = self
.get_asset_inputs_for_amount(asset_id, amount, None)
.await?;
let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount);
let consensus_parameters = self.try_provider()?.consensus_parameters();
let tx_builder = ScriptTransactionBuilder::prepare_transfer(inputs, outputs, tx_parameters)
.set_consensus_parameters(consensus_parameters);
let previous_base_amount = if asset_id == AssetId::default() {
amount
} else {
0
};
let tx = self
.add_fee_resources(tx_builder, previous_base_amount, None)
.await?;
let receipts = self.try_provider()?.send_transaction(&tx).await?;
Ok((tx.id(consensus_parameters.chain_id.into()), receipts))
}
async fn force_transfer_to_contract(
&self,
to: &Bech32ContractId,
balance: u64,
asset_id: AssetId,
tx_parameters: TxParameters,
) -> std::result::Result<(String, Vec<Receipt>), Error> {
let zeroes = Bytes32::zeroed();
let plain_contract_id: ContractId = to.into();
let mut inputs = vec![Input::contract(
UtxoId::new(zeroes, 0),
zeroes,
zeroes,
TxPointer::default(),
plain_contract_id,
)];
inputs.extend(
self.get_asset_inputs_for_amount(asset_id, balance, None)
.await?,
);
let outputs = vec![
Output::contract(0, zeroes, zeroes),
Output::change(self.address().into(), 0, asset_id),
];
let params = self.try_provider()?.consensus_parameters();
let tb = ScriptTransactionBuilder::prepare_contract_transfer(
plain_contract_id,
balance,
asset_id,
inputs,
outputs,
tx_parameters,
)
.set_consensus_parameters(params);
let base_amount = if asset_id == AssetId::default() {
balance
} else {
0
};
let tx = self.add_fee_resources(tb, base_amount, None).await?;
let tx_id = tx.id(params.chain_id.into());
let receipts = self.try_provider()?.send_transaction(&tx).await?;
Ok((tx_id.to_string(), receipts))
}
async fn withdraw_to_base_layer(
&self,
to: &Bech32Address,
amount: u64,
tx_parameters: TxParameters,
) -> std::result::Result<(TxId, MessageId, Vec<Receipt>), Error> {
let params = self.try_provider()?.consensus_parameters();
let chain_id = params.chain_id;
let inputs = self
.get_asset_inputs_for_amount(BASE_ASSET_ID, amount, None)
.await?;
let tb = ScriptTransactionBuilder::prepare_message_to_output(
to.into(),
amount,
inputs,
tx_parameters,
);
let tx = self.add_fee_resources(tb, amount, None).await?;
let tx_id = tx.id(chain_id.into());
let receipts = self.try_provider()?.send_transaction(&tx).await?;
let message_id = extract_message_id(&receipts)
.expect("MessageId could not be retrieved from tx receipts.");
Ok((tx_id, message_id, receipts))
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use fuel_core_client::client::FuelClient;
use fuel_crypto::{Message, SecretKey};
use fuel_tx::{Address, ConsensusParameters, Output};
use fuels_core::types::transaction::Transaction;
use rand::{rngs::StdRng, RngCore, SeedableRng};
use super::*;
use crate::wallet::WalletUnlocked;
#[tokio::test]
async fn sign_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
let mut rng = StdRng::seed_from_u64(2322u64);
let mut secret_seed = [0u8; 32];
rng.fill_bytes(&mut secret_seed);
let secret = secret_seed
.as_slice()
.try_into()
.expect("The seed size is valid");
let wallet = WalletUnlocked::new_from_private_key(secret, None);
let message = "my message";
let signature = wallet.sign_message(message).await?;
assert_eq!(signature, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d")?);
let message = Message::new(message);
let recovered_address = signature.recover(&message)?;
assert_eq!(wallet.address().hash(), recovered_address.hash());
signature.verify(&recovered_address, &message)?;
Ok(())
}
#[tokio::test]
async fn sign_tx_and_verify() -> std::result::Result<(), Box<dyn std::error::Error>> {
let secret = SecretKey::from_str(
"5f70feeff1f229e4a95e1056e8b4d80d0b24b565674860cc213bdb07127ce1b1",
)?;
let mut wallet = WalletUnlocked::new_from_private_key(secret, None);
let coin = Coin {
amount: 10000000,
owner: wallet.address().clone(),
..Default::default()
};
let input_coin = Input::ResourceSigned {
resource: CoinType::Coin(coin),
witness_index: 0,
};
let output_coin = Output::coin(
Address::from_str(
"0xc7862855b418ba8f58878db434b21053a61a2025209889cc115989e8040ff077",
)?,
1,
Default::default(),
);
let mut tx = ScriptTransactionBuilder::prepare_transfer(
vec![input_coin],
vec![output_coin],
Default::default(),
)
.build()?;
let consensus_parameters = ConsensusParameters::default();
let test_provider = Provider::new(FuelClient::new("test")?, consensus_parameters);
wallet.set_provider(test_provider);
let signature = wallet.sign_transaction(&mut tx)?;
let message = Message::from_bytes(*tx.id(consensus_parameters.chain_id.into()));
assert_eq!(signature, Signature::from_str("df91e8ae723165f9a28b70910e3da41300da413607065618522f3084c9f051114acb1b51a836bd63c3d84a1ac904bf37b82ef03973c19026b266d04872f170a6")?);
let recovered_address = signature.recover(&message)?;
assert_eq!(wallet.address().hash(), recovered_address.hash());
signature.verify(&recovered_address, &message)?;
Ok(())
}
}