Let's jump back into our MultiSig project again!
cd ../../multisig-predicate/predicate
Again follow these steps with cargo-generate
in the predicate project directory like we did previously:
cargo-generate
: cargo install cargo-generate --locked
cargo generate --init fuellabs/sway templates/sway-test-rs --name sway-store
Delete the templated code and copy the following imports into your harness file. It's important to pay attention to two main imports: predicates
, for obvious reasons, and the ScriptTransactionBuilder
, which we'll use to create transactions. These transactions must be signed before being broadcasted to our local network.
use fuels::{
crypto::SecretKey,
accounts::{
predicate::Predicate,
wallet::WalletUnlocked,
Account,
},
prelude::*,
types::transaction_builders::{ScriptTransactionBuilder, BuildableTransaction},
};
Similar to Rust testing for contracts, we'll import the predicate ABI (Application Binary Interface) to interact with it. Ensure the name of your predicate matches the one you're working with.
abigen!(Predicate(
name = "MultiSig",
abi = "./out/debug/predicate-abi.json"
));
If you're familiar with Rust testing for Sway projects, much of the setup will be similar. Copy and paste the setup_wallets_and_network
function into your harness file.
async fn setup_wallets_and_network() -> (Vec<WalletUnlocked>, Provider, AssetId) {
// WALLETS
let private_key_0: SecretKey =
"0xc2620849458064e8f1eb2bc4c459f473695b443ac3134c82ddd4fd992bd138fd"
.parse()
.unwrap();
let private_key_1: SecretKey =
"0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd"
.parse()
.unwrap();
let private_key_2: SecretKey =
"0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb"
.parse()
.unwrap();
let mut wallet_0: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_0, None);
let mut wallet_1: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_1, None);
let mut wallet_2: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_2, None);
// TOKENS
let asset_id = AssetId::default();
let all_coins = [&wallet_0, &wallet_1, &wallet_2]
.iter()
.flat_map(|wallet| {
setup_single_asset_coins(wallet.address(), AssetId::default(), 10, 1_000_000)
})
.collect::<Vec<_>>();
// NETWORKS
let node_config = NodeConfig::default();
let provider = setup_test_provider(all_coins, vec![], Some(node_config), None).await.unwrap();
[&mut wallet_0, &mut wallet_1, &mut wallet_2]
.iter_mut()
.for_each(|wallet| {
wallet.set_provider(provider.clone());
});
return (
vec![wallet_0, wallet_1, wallet_2],
provider,
asset_id,
);
}
The three key setup steps include:
// WALLETS
let private_key_0: SecretKey =
"0xc2620849458064e8f1eb2bc4c459f473695b443ac3134c82ddd4fd992bd138fd"
.parse()
.unwrap();
let private_key_1: SecretKey =
"0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd"
.parse()
.unwrap();
let private_key_2: SecretKey =
"0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb"
.parse()
.unwrap();
let mut wallet_0: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_0, None);
let mut wallet_1: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_1, None);
let mut wallet_2: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_2, None);
// TOKENS
let asset_id = AssetId::default();
let all_coins = [&wallet_0, &wallet_1, &wallet_2]
.iter()
.flat_map(|wallet| {
setup_single_asset_coins(wallet.address(), AssetId::default(), 10, 1_000_000)
})
.collect::<Vec<_>>();
// NETWORKS
let node_config = NodeConfig::default();
let provider = setup_test_provider(all_coins, vec![], Some(node_config), None).await.unwrap();
Since the predicate address is deterministic, we don't need to copy it as we do with smart contracts, which are deployed with a different address each time. We can leverage SDKs to build the predicate, ensuring we're working with the correct address without error!
Now, let's review the sequence of actions we'll take to simulate a real-world scenario, copy and paste the first test below and let's break it down step by step:
#[tokio::test]
async fn multisig_two_of_three() -> Result<()> {
let (wallets, provider, asset_id) = setup_wallets_and_network().await;
// CONFIGURABLES
let required_signatures = 2;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
// FUND PREDICATE
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
// BUILD TRANSACTION
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
// SIGN TRANSACTION
tb.add_signer(wallets[0].clone())?;
tb.add_signer(wallets[1].clone())?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, multisig_amount);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount - multisig_amount);
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
provider.send_transaction_and_await_commit(tx).await?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, 0);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount);
Ok(())
}
For step 1, as mentioned earlier, when we configure the number of required signatures (up to 3) and the 3 addresses that will safeguard our funds. Importing the ABI will automatically load a PredicateNameConfigurable
type. In our case, that will be MultiSigConfigurables
. There will be a corresponding with_configurable function to help you load each configurable. In our case, with_REQUIRED_SIGNATURES
and with_SIGNERS
are both loaded in!
How convenient!
// CONFIGURABLES
let required_signatures = 2;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
Next, we'll load our original predicate binary with our new configurables to generate our personalized predicate instance. Simply input your configurables using the with_configurables
function, and this will give us a unique predicate root based on our inputs.
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
For step 2, transferring funds to our newly generated predicate root is as straightforward as any other blockchain transfer.
// FUND PREDICATE
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
In step 3, when the multisig holders decide to use the locked funds, we build a transaction specifying the inputs and outputs. Pay close attention to the outputs; we need to specify where the tokens from the predicate are going, which native asset they involve, and the amount. We're essentially extracting a portion of the original base asset sent into the predicate.
// BUILD TRANSACTION
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
The correct wallet addresses configured in the configurables must sign the transactions. This information, loaded as witness data, will evaluate our predicate to true. It's crucial to provide enough correct, unique signatures; otherwise, the transaction will fail, as demonstrated in later tests. Since our test only requires 2 signatures, we need to provide just those.
// SIGN TRANSACTION
tb.add_signer(wallets[0].clone())?;
tb.add_signer(wallets[1].clone())?;
After the evaluation is correctly done, all we need to do is broadcast the transaction, and the requested funds should return to wallet 1.
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
provider.send_transaction_and_await_commit(tx).await?;
The setup for the second test, multisig_mixed_three_of_three
, follows the same scheme, showcasing that the transaction signing can be done in any order by valid wallets.
#[tokio::test]
async fn multisig_mixed_three_of_three() -> Result<()> {
let (wallets, provider, asset_id) = setup_wallets_and_network().await;
// CONFIGURABLES
let required_signatures = 3;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
// NOTE Cannot be signed in any order
tb.add_signer(wallets[2].clone())?;
tb.add_signer(wallets[0].clone())?;
tb.add_signer(wallets[1].clone())?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, multisig_amount);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount - multisig_amount);
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
provider.send_transaction_and_await_commit(tx).await?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, 0);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount);
Ok(())
}
The same principle applies to the third test, multisig_not_enough_signatures_fails
, where the transaction will fail if there aren't enough signatures.
#[tokio::test]
async fn multisig_not_enough_signatures_fails() -> Result<()> {
let (wallets, provider, asset_id) = setup_wallets_and_network().await;
// CONFIGURABLES
let required_signatures = 2;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
tb.add_signer(wallets[0].clone())?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, multisig_amount);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount - multisig_amount);
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
let _ = provider.send_transaction_and_await_commit(tx).await.is_err();
Ok(())
}
If you have followed the previous steps correctly, your harness.rs
test file should look like this:
use fuels::{
crypto::SecretKey,
accounts::{
predicate::Predicate,
wallet::WalletUnlocked,
Account,
},
prelude::*,
types::transaction_builders::{ScriptTransactionBuilder, BuildableTransaction},
};
abigen!(Predicate(
name = "MultiSig",
abi = "./out/debug/predicate-abi.json"
));
async fn setup_wallets_and_network() -> (Vec<WalletUnlocked>, Provider, AssetId) {
// WALLETS
let private_key_0: SecretKey =
"0xc2620849458064e8f1eb2bc4c459f473695b443ac3134c82ddd4fd992bd138fd"
.parse()
.unwrap();
let private_key_1: SecretKey =
"0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd"
.parse()
.unwrap();
let private_key_2: SecretKey =
"0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb"
.parse()
.unwrap();
let mut wallet_0: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_0, None);
let mut wallet_1: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_1, None);
let mut wallet_2: WalletUnlocked = WalletUnlocked::new_from_private_key(private_key_2, None);
// TOKENS
let asset_id = AssetId::default();
let all_coins = [&wallet_0, &wallet_1, &wallet_2]
.iter()
.flat_map(|wallet| {
setup_single_asset_coins(wallet.address(), AssetId::default(), 10, 1_000_000)
})
.collect::<Vec<_>>();
// NETWORKS
let node_config = NodeConfig::default();
let provider = setup_test_provider(all_coins, vec![], Some(node_config), None).await.unwrap();
[&mut wallet_0, &mut wallet_1, &mut wallet_2]
.iter_mut()
.for_each(|wallet| {
wallet.set_provider(provider.clone());
});
return (
vec![wallet_0, wallet_1, wallet_2],
provider,
asset_id,
);
}
#[tokio::test]
async fn multisig_two_of_three() -> Result<()> {
let (wallets, provider, asset_id) = setup_wallets_and_network().await;
// CONFIGURABLES
let required_signatures = 2;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
// FUND PREDICATE
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
// BUILD TRANSACTION
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
// SIGN TRANSACTION
tb.add_signer(wallets[0].clone())?;
tb.add_signer(wallets[1].clone())?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, multisig_amount);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount - multisig_amount);
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
provider.send_transaction_and_await_commit(tx).await?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, 0);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount);
Ok(())
}
#[tokio::test]
async fn multisig_mixed_three_of_three() -> Result<()> {
let (wallets, provider, asset_id) = setup_wallets_and_network().await;
// CONFIGURABLES
let required_signatures = 3;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
// NOTE Cannot be signed in any order
tb.add_signer(wallets[2].clone())?;
tb.add_signer(wallets[0].clone())?;
tb.add_signer(wallets[1].clone())?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, multisig_amount);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount - multisig_amount);
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
provider.send_transaction_and_await_commit(tx).await?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, 0);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount);
Ok(())
}
#[tokio::test]
async fn multisig_not_enough_signatures_fails() -> Result<()> {
let (wallets, provider, asset_id) = setup_wallets_and_network().await;
// CONFIGURABLES
let required_signatures = 2;
let signers: [Address; 3] = [
wallets[0].address().into(),
wallets[1].address().into(),
wallets[2].address().into(),
];
let configurables = MultiSigConfigurables::default()
.with_REQUIRED_SIGNATURES(required_signatures)?
.with_SIGNERS(signers)?;
// PREDICATE
let predicate_binary_path = "./out/debug/predicate.bin";
let predicate: Predicate = Predicate::load_from(predicate_binary_path)?
.with_provider(provider.clone())
.with_configurables(configurables);
let multisig_amount = 100;
let wallet_0_amount = provider.get_asset_balance(wallets[0].address(), asset_id).await?;
wallets[0]
.transfer(predicate.address(), multisig_amount, asset_id, TxPolicies::default())
.await?;
let mut tb: ScriptTransactionBuilder = {
let input_coin = predicate.get_asset_inputs_for_amount(asset_id, 1).await?;
let output_coin =
predicate.get_asset_outputs_for_amount(wallets[0].address().into(), asset_id, multisig_amount);
ScriptTransactionBuilder::prepare_transfer(
input_coin,
output_coin,
TxPolicies::default(),
)
};
tb.add_signer(wallets[0].clone())?;
assert_eq!(provider.get_asset_balance(predicate.address(), asset_id).await?, multisig_amount);
assert_eq!(provider.get_asset_balance(wallets[0].address(), asset_id).await?, wallet_0_amount - multisig_amount);
// SPEND PREDICATE
let tx: ScriptTransaction = tb.build(provider.clone()).await?;
let _ = provider.send_transaction_and_await_commit(tx).await.is_err();
Ok(())
}
To run the test located in tests/harness.rs
, use:
cargo test
If you want to print outputs to the console during tests, use the nocapture
flag:
cargo test -- --nocapture
Congratulations on making it this far! We've confirmed that our Multisig works.
Predicates aren't meant to be intimidating. State-minimized DeFi applications should be the standard, rather than resorting to gas golfing or writing assembly code for these optimizations. Now that you have predicates in your toolbox, go out and explore what other state-minimized DeFi applications you can build!