Call Multiple Contracts in a Single Tx
Up to now, we have only shown how to call one smart contract in a transaction. That is, only one input of the tx spends a smart contract UTXO, and the other inputs, if any, spend Pay-to-Public-Key-Hash (P2PKH) UTXOs, which are generally NOT regarded as smart contracts.
There are cases where it is desirable to spend multiple smart contract UTXOs in different inputs of a tx.
The main differences from calling a single contract are:
- Set
multiContractCall = true
inMethodCallOptions
- Each call may only return a partial/incomplete transaction, instead of a complete transaction
- A partial tx has to be passed as
ContractTransaction
inMethodCallOptions
in subsequent calls - Finally invoke
SmartContract.multiContractCall(partialContractTx: ContractTransaction, signer: Signer)
to sign and broadcast the complete transaction
The following is an example code of calling two contracts at the same time:
import { Counter } from '../../src/contracts/counter'
import { getDefaultSigner } from '../utils/helper'
import { HashPuzzle } from '../../src/contracts/hashPuzzle'
async function main() {
await Counter.loadArtifact()
await HashPuzzle.loadArtifact()
const signer = getDefaultSigner()
let counter = new Counter(1n)
// connect to a signer
await counter.connect(signer)
// contract deployment
const deployTx = await counter.deploy(1)
console.log('Counter contract deployed: ', deployTx.id)
counter.bindTxBuilder(
'incrementOnChain',
(
current: Counter,
options: MethodCallOptions<Counter>,
...args: any
): Promise<ContractTransaction> => {
// create the next instance from the current
const nextInstance = current.next()
// apply updates on the next instance locally
nextInstance.count++
const tx = new bsv.Transaction()
tx.addInput(current.buildContractInput()).addOutput(
new bsv.Transaction.Output({
script: nextInstance.lockingScript,
satoshis: current.balance,
})
)
return Promise.resolve({
tx: tx,
atInputIndex: 0,
nexts: [
{
instance: nextInstance,
balance: current.balance,
atOutputIndex: 0,
},
],
})
}
)
const plainText = 'abc'
const byteString = toByteString(plainText, true)
const sha256Data = sha256(byteString)
const hashPuzzle = new HashPuzzle(sha256Data)
// connect to a signer
await hashPuzzle.connect(signer)
const deployTx1 = await hashPuzzle.deploy(1)
console.log('HashPuzzle contract deployed: ', deployTx1.id)
hashPuzzle.bindTxBuilder(
'unlock',
(
current: HashPuzzle,
options: MethodCallOptions<HashPuzzle>,
...args: any
): Promise<ContractTransaction> => {
if (options.partialContractTx) {
const unSignedTx = options.partialContractTx.tx
unSignedTx.addInput(
current.buildContractInput()
)
if (options.changeAddress) {
unSignedTx.change(options.changeAddress)
}
return Promise.resolve({
tx: unSignedTx,
atInputIndex: 1,
nexts: [],
})
}
throw new Error('no partialContractTx found')
}
)
const partialTx = await counter.methods.incrementOnChain({
multiContractCall: true,
} as MethodCallOptions<Counter>)
const finalTx = await hashPuzzle.methods.unlock(
byteString,
{
multiContractCall: true,
partialContractTx: partialTx,
changeAddress: await signer.getDefaultAddress()
} as MethodCallOptions<HashPuzzle>
)
const { tx: callTx, nexts } = await SmartContract.multiContractCall(
finalTx,
signer
)
console.log('Counter, HashPuzzle contract `unlock` called: ', callTx.id)
// hashPuzzle has terminated, but counter can still be called
counter = nexts[0].instance
}
await main()
note
- You must bind a transaction builder to each contract instance, since the default only spends a single contract UTXO.
- If the called contracts need signatures from different private keys to be called, the signer passed to
multiContractCall
must have all private keys.