Skip to content

Transactions with Multiple Signers ​

When a transaction contains a spendable input such as a coin, it must also contain the signature of the coin owner for it to be spent. If the coin owner is also submitting the transaction, then this is straightforward. However, if an external address is required to sign the transaction, then the transaction must contain multiple signatures. Within the SDK, an account signature can be added to a transaction by calling addAccountWitnesses on the transaction request.

Consider a script that requires two signatures to be spent:

rust
script;

use std::{b512::B512, ecr::ec_recover_address, tx::{tx_id, tx_witness_data}};

fn main(signer: b256) -> bool {
    let witness_data: B512 = tx_witness_data(1).unwrap();
    let address: b256 = ec_recover_address(witness_data, tx_id()).unwrap().bits();
    return address == signer;
}
See code in context

In the snippet above, we use the built-in Sway function tx_witness_data() to retrieve the witness signatures and tx_id() for the transaction hash. Then, we retrieve the signing address to validate the script.

We would interact with this script in the SDK by creating a transaction request from an invocation scope. The same can be done for a contract. Consider the following script:

ts
import type { BN } from 'fuels';
import { Script, Provider, Wallet } from 'fuels';

import {
  LOCAL_NETWORK_URL,
  WALLET_PVT_KEY,
  WALLET_PVT_KEY_2,
  WALLET_PVT_KEY_3,
} from '../../env';
import { ScriptSigning } from '../../typegend';

const provider = await Provider.create(LOCAL_NETWORK_URL);
const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const signer = Wallet.fromPrivateKey(WALLET_PVT_KEY_2, provider);
const receiver = Wallet.fromPrivateKey(WALLET_PVT_KEY_3, provider);

const amountToReceiver = 100;

const script = new Script(ScriptSigning.bytecode, ScriptSigning.abi, sender);
const { waitForResult } = await script.functions
  .main(signer.address.toB256())
  .addTransfer({
    destination: receiver.address,
    amount: amountToReceiver,
    assetId: provider.getBaseAssetId(),
  })
  .addSigners(signer)
  .call<BN>();

const { value } = await waitForResult();
See code in context

The same approach can be used for a predicate by instantiating it and adding it to a transaction request. Consider the following predicate:

rust
predicate;

use std::{b512::B512, ecr::ec_recover_address, tx::{tx_id, tx_witness_data}};

fn main(signer: b256) -> bool {
    let witness_data: B512 = tx_witness_data(1).unwrap();
    let address: b256 = ec_recover_address(witness_data, tx_id()).unwrap().bits();
    return address == signer;
}
See code in context

We can interact with this predicate in the SDK with the following implementation:

ts
import { Predicate, Provider, ScriptTransactionRequest, Wallet } from 'fuels';

import {
  LOCAL_NETWORK_URL,
  WALLET_PVT_KEY,
  WALLET_PVT_KEY_2,
  WALLET_PVT_KEY_3,
} from '../../env';
import { PredicateSigning } from '../../typegend';

const provider = await Provider.create(LOCAL_NETWORK_URL);

const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const signer = Wallet.fromPrivateKey(WALLET_PVT_KEY_2, provider);
const receiver = Wallet.fromPrivateKey(WALLET_PVT_KEY_3, provider);

const amountToReceiver = 100;

// Create and fund the predicate
const predicate = new Predicate<[string]>({
  bytecode: PredicateSigning.bytecode,
  abi: PredicateSigning.abi,
  provider,
  data: [signer.address.toB256()],
});
const tx = await sender.transfer(
  predicate.address,
  200_000,
  provider.getBaseAssetId()
);
await tx.waitForResult();

// Create the transaction request
const request = new ScriptTransactionRequest();
request.addCoinOutput(
  receiver.address,
  amountToReceiver,
  provider.getBaseAssetId()
);

// Get the predicate resources and add them and predicate data to the request
const resources = await predicate.getResourcesToSpend([
  {
    assetId: provider.getBaseAssetId(),
    amount: amountToReceiver,
  },
]);

request.addResources(resources);
request.addWitness('0x');

// Add witnesses including the signer
// Estimate the predicate inputs
const txCost = await predicate.getTransactionCost(request, {
  signatureCallback: (txRequest) => txRequest.addAccountWitnesses(signer),
});

request.updatePredicateGasUsed(txCost.estimatedPredicates);

request.gasLimit = txCost.gasUsed;
request.maxFee = txCost.maxFee;

await predicate.fund(request, txCost);

await request.addAccountWitnesses(signer);

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