Skip to content

Send And Spend Funds From Predicates

Predicates can be used to validate transactions. This implies that a predicate can safeguard assets, only allowing their transfer if the predicate conditions are met.

This guide will demonstrate how to send and spend funds using a predicate.

Predicate Example

Consider the following predicate:

rust
predicate;

fn main(input_address: b256) -> bool {
    let valid_address = 0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4;

    input_address == valid_address
}
See code in context

This predicate accepts an address of type b256 and compares it with a hard-coded address of the same type. If both addresses are equal, the predicate returns true, otherwise it will return false.

Interacting with the Predicate Using SDK

Let's use the above predicate to validate our transaction.

Once you've compiled the predicate (forc build), you'll obtain two important artifacts: the JSON ABI and the predicate's binary code. These are needed to instantiate a new predicate.

This is where we also pass in the predicate's data. Note that the main function in our predicate example requires a parameter called input_address of type b256. We will pass this parameter to the Predicate constructor along with the bytecode and the JSON ABI.

ts
const inputAddress = '0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4';

const predicate = new Predicate({
  bytecode: SimplePredicate.bytecode,
  provider,
  abi: SimplePredicate.abi,
  data: [inputAddress],
});
See code in context

Note: If you want to pass in the predicate data after instantiating the Predicate or if you want to use a different data than the one passed in the constructor, you will have to create a new Predicate instance.

With the predicate instantiated, we can transfer funds to its address. This requires us to have a wallet with sufficient funds. If you're unsure about using wallets with the SDK, we recommend checking out our wallet guide.

ts
const amountToPredicate = 10_000_000;
const amountToReceiver = 200;
const tx = await walletWithFunds.transfer(
  predicate.address,
  amountToPredicate,
  provider.getBaseAssetId(),
  {
    gasLimit: 1000,
  }
);

let { isStatusSuccess } = await tx.waitForResult();
expect(isStatusSuccess).toBeTruthy();
See code in context

Now that our predicate holds funds, we can use it to validate a transaction and hence execute our transfer. We can achieve that by doing the following:

ts
const receiverWallet = WalletUnlocked.generate({
  provider,
});

const receiverInitialBalance = await receiverWallet.getBalance();

const tx2 = await predicate.transfer(
  receiverWallet.address.toB256(),
  amountToReceiver,
  provider.getBaseAssetId()
);

({ isStatusSuccess } = await tx2.waitForResult());
expect(isStatusSuccess).toBeTruthy();

const receiverFinalBalance = await receiverWallet.getBalance();
expect(receiverFinalBalance.gt(receiverInitialBalance)).toBeTruthy();

({ isStatusSuccess } = await tx2.waitForResult());
expect(isStatusSuccess).toBeTruthy();
See code in context

Note the method transfer has two parameters: the recipient's address and the intended transfer amount.

Once the predicate resolves with a return value true based on its predefined condition, our predicate successfully spends its funds by means of a transfer to a desired wallet.


In a similar approach, you can use the createTransfer method, which returns a ScriptTransactionRequest.

ts
const transactionRequest = await predicate.createTransfer(
  receiverWallet.address,
  amountToReceiver,
  provider.getBaseAssetId(),
  {
    gasLimit: 1000,
  }
);

/*
  You can retrieve the transaction ID before actually submitting it to the node
  like this:
 */

const chainId = provider.getChainId();
const txId = transactionRequest.getTransactionId(chainId);

const res = await predicate.sendTransaction(transactionRequest);

await res.waitForResult();
See code in context

Spending Entire Predicate Held Amount

Trying to forward the entire amount held by the predicate results in an error because no funds are left to cover the transaction fees. Attempting this will result in an error message like:

ts
const errorMsg = `The account(s) sending the transaction don't have enough funds to cover the transaction.`;
See code in context

Predicate Validation Failure

What happens when a predicate fails to validate? Recall our predicate only validates if the input_address matches the hard-coded valid_address. Hence, if we set a different data from the valid_address, the predicate will fail to validate.

When a predicate fails to validate, the SDK throws an error that starts like this:

ts
const errorMsg = 'PredicateVerificationFailed';
See code in context

Pre-staging a Transaction

In some cases, you may want to pre-stage a predicate transaction before submitting it for execution. To do this, you can use the createTransfer method on the Predicate class.

In the following example, we are pre-staging a transaction to be able to know the transaction ID without actually submitting the transaction.

ts
// Prepare the transaction
const preparedTx = await predicate.createTransfer(
  receiverWallet.address,
  transferAmount,
  provider.getBaseAssetId()
);

// Get the transaction ID before sending the transaction
const txId = preparedTx.getTransactionId(provider.getChainId());

// Send the transaction
const res = await predicate.sendTransaction(preparedTx);
await res.waitForResult();
See code in context