Send and spend funds from predicates

Let's consider the following predicate example:

predicate;

use std::{b512::B512, constants::ZERO_B256, ecr::ec_recover_address, inputs::input_predicate_data};

fn extract_pulic_key_and_match(signature: B512, expected_public_key: b256) -> u64 {
    if let Result::Ok(pub_key_sig) = ec_recover_address(signature, ZERO_B256)
    {
        if pub_key_sig.value == expected_public_key {
            return 1;
        }
    }
    0
}

fn main() -> bool {
    let signatures: [B512; 3] = input_predicate_data(0);

    let public_keys = [
        0xd58573593432a30a800f97ad32f877425c223a9e427ab557aab5d5bb89156db0,
        0x14df7c7e4e662db31fe2763b1734a3d680e7b743516319a49baaa22b2032a857,
        0x3ff494fb136978c3125844625dad6baf6e87cdb1328c8a51f35bda5afe72425c,
    ];

    let mut matched_keys = 0;

    matched_keys = extract_pulic_key_and_match(signatures[0], public_keys[0]);
    matched_keys = matched_keys + extract_pulic_key_and_match(signatures[1], public_keys[1]);
    matched_keys = matched_keys + extract_pulic_key_and_match(signatures[2], public_keys[2]);

    matched_keys > 1
}

This predicate accepts three signatures and matches them to three predefined public keys. The ec_recover_address function is used to recover the public key from the signatures. If two of three extracted public keys match the predefined public keys, the funds can be spent. Note that the signature order has to match the order of the predefined public keys.

Let's use the SDK to interact with the predicate. First, let's create three wallets with specific keys. Their hashed public keys are already hard-coded in the predicate.

        let secret_key1: SecretKey =
            "0x862512a2363db2b3a375c0d4bbbd27172180d89f23f2e259bac850ab02619301"
                .parse()
                .unwrap();

        let secret_key2: SecretKey =
            "0x37fa81c84ccd547c30c176b118d5cb892bdb113e8e80141f266519422ef9eefd"
                .parse()
                .unwrap();

        let secret_key3: SecretKey =
            "0x976e5c3fa620092c718d852ca703b6da9e3075b9f2ecb8ed42d9f746bf26aafb"
                .parse()
                .unwrap();

        let mut wallet = WalletUnlocked::new_from_private_key(secret_key1, None);
        let mut wallet2 = WalletUnlocked::new_from_private_key(secret_key2, None);
        let mut wallet3 = WalletUnlocked::new_from_private_key(secret_key3, None);
        let receiver = WalletUnlocked::new_random(None);

Next, let's add some coins, start a provider and connect it with the wallets.

        let all_coins = [&wallet, &wallet2, &wallet3]
            .iter()
            .flat_map(|wallet| {
                setup_single_asset_coins(wallet.address(), AssetId::default(), 10, 1_000_000)
            })
            .collect::<Vec<_>>();

        let (provider, _) = setup_test_provider(
            all_coins,
            vec![],
            Some(Config {
                utxo_validation: true,
                ..Config::local_node()
            }),
            None,
        )
        .await;

        [&mut wallet, &mut wallet2, &mut wallet3]
            .iter_mut()
            .for_each(|wallet| wallet.set_provider(provider.clone()));

Now we can load the predicate binary, and prepare some transaction variables.

        let predicate = Predicate::load_from(
            "../../packages/fuels/tests/predicates/predicate_signatures/out/debug/predicate_signatures.bin",
        )?;

        let predicate_code = predicate.code();
        let predicate_address = predicate.address();
        let amount_to_predicate = 1000;
        let asset_id = AssetId::default();

After the predicate address is generated we can send funds to it. Note that we are using the same transfer function as we used when sending funds to other wallets. We also make sure that the funds are indeed transferred.

        wallet
            .transfer(
                predicate_address,
                amount_to_predicate,
                asset_id,
                TxParameters::default(),
            )
            .await?;

        let predicate_balance = provider
            .get_asset_balance(predicate.address(), asset_id)
            .await?;
        assert_eq!(predicate_balance, amount_to_predicate);

To spend the funds that are now locked in the predicate, we have to provide two out of three signatures whose public keys match the ones we defined in the predicate. In this example, the signatures are generated from an array of zeros.

        let data_to_sign = [0; 32];
        let signature1 = wallet.sign_message(data_to_sign).await?.to_vec();
        let signature2 = wallet2.sign_message(data_to_sign).await?.to_vec();
        let signature3 = wallet3.sign_message(data_to_sign).await?.to_vec();

        let signatures = vec![signature1, signature2, signature3];

After generating the signatures, we can send a transaction to spend the predicate funds. We use the receiver wallet as the recipient. We have to provide the predicate byte code and the required signatures. As we provide the correct data, we receive the funds and verify that the amount is correct.

        let predicate_data = signatures.into_iter().flatten().collect();
        wallet
            .spend_predicate(
                predicate_address,
                predicate_code,
                amount_to_predicate,
                asset_id,
                receiver.address(),
                Some(predicate_data),
                TxParameters::default(),
            )
            .await?;

        let receiver_balance_after = provider
            .get_asset_balance(receiver.address(), asset_id)
            .await?;
        assert_eq!(amount_to_predicate, receiver_balance_after);

        let predicate_balance = provider
            .get_asset_balance(predicate.address(), asset_id)
            .await?;
        assert_eq!(predicate_balance, 0);