How to Deploy & Call a Contract
Core Concepts
After you've finished writing a contract, you can deploy and call it. But first, you should learn how a smart contract interacts with the blockchain. In this section, we will go over some fundamental concepts in detail.
Compile and Load Contract
First, compile the contract using the CLI:
npx scrypt-cli compile
This will create an artifact json file of your contract in the /artifacts
folder.
Next, call loadArtifact to load the json file, so you have a smart contract class ready to be instantiated.
import artifact from '../artifacts/mycontract.json'
await MyContract.loadArtifact(artifact)
Contract Instance
As explained in the Overview section, an sCrypt
contract is based on the Bitcoin UTXO model. A constract instance is an abstraction that represents a specific contract deployed on-chain, so you can use it to interact with the contract like a normal TypeScript object.
// construct a new instance of `MyContract`
let instance = new MyContract(...initArgs);
Provider
A Provider
is an abstraction of a standard Bitcoin node that provides connection to the Bitcoin network, for read and write access to the blockchain.
sCrypt already has a few built-in providers:
DummyProvider
: A mockup provider just for local tests. It does not connect to the Bitcoin blockchain and thus cannot send transactions.DefaultProvider
: The default provider is the safest, easiest way to begin developing on Bitcoin, and it is also robust enough for use in production. It can be used in testnet as well as mainnet.See a list of available providers.
You can initialize these providers like this:
let dummyProvider = new DummyProvider();
// mainnet
let provider = new DefaultProvider();
// testnet
let provider = new DefaultProvider(bsv.Networks.testnet);
Signer
A Signer
is an abstraction of private keys, which can be used to sign messages and transactions. A simple signer would be a single private key, while a complex signer is a wallet.
TestWallet
For testing purposes only, we have a built-in wallet called TestWallet
. It can be created like this:
const signer = new TestWallet(privateKey, provider);
privateKey
can be a single private key or an array of private keys that the wallet can use to sign transactions. The ability of the wallet to send transactions is assigned to provider
. In other words, a TestWallet
serves as both a signer and a provider.
Tx Builders
To deploy or interact with contracts, we must build transactions and broadcast them to Bitcoin. We have some built-in tx builders for the most common way to interact with contracts, so usually you don't have to implement them. If the default tx builder does not meet your specific requirements, such as having extra inputs or outputs in your tx, you can customize it.
Contract Deployment Transaction
A Bitcoin transaction is required when deploying a contract to the blockchain. The transaction should have an output, whose script is compiled from the contract. This output is known as a contract UTXO and the contract instance comes from
this UTXO.
An instance's from
can be accessed.
// the tx that contains the instance
instance.from.tx
// the index of the tx output that contains the instance
instance.from.outputIndex
Contract Call Transaction
When you call a public method of a contract instance in a UTXO, a call transaction is needed. The transaction has an input that references to the UTXO and contains the script consisting of the method's arguments. We regard the contract instance goes to
this transaction input.
An instance's to
can be accessed.
// the tx that spends the instance
instance.to.tx
// the index of the tx input that spends the UTXO the instance is in
instance.to.inputIndex
This section could be summarized as the diagram below:
Prepare a Signer and Provider
A signer and a provider must be connected to a contract instance before deployment and call. When we are ready to deploy the contract to the testnet/mainnet, we need a real provider like DefaultProvider.
const network = bsv.Networks.testnet; // or bsv.Networks.mainnet
const signer = new TestWallet(privateKey, new DefaultProvider(network));
The privateKey
must have enough coins. Learn how to fund it on a testnet using a faucet.
Then, connect it to your contract instance like this:
await instance.connect(signer);
TestWallet
is just a Signer
provided by sCrypt for testing. In a real production environment (mainnet), you should use PandaSigner
, SensiletSigner
, DotwalletSigner
, TAALSigner
.
See here how to use them.
Contract Deployment
To deploy a smart contract, call its deploy
method, like this:
// construct a new instance of `MyContract`
let instance = new MyContract(...initArgs);
// connect the signer to the instance
await instance.connect(signer);
// the contract UTXO’s satoshis
const initBalance = 1234;
// build and send tx for deployment
const deployTx = await instance.deploy(initBalance);
console.log(`Smart contract successfully deployed with txid ${deployTx.id}`);
Contract Call
To facilitate calling a contract's public @method
, we have injected a runtime object named methods
in your contract class. For each public @method
of your contract (e.g., contract.foo
), a function with the same name and signature (including list of parameters and return type, i.e., void) is added into methods
(e.g., contract.methods.foo
). In addition, there is an options
appended as the last paramter.
Assume you have a contract like this:
Class MyContract extends SmartContract {
...
@method()
public foo(arg1, arg2) {...}
}
You can check it like this:
let instance = new MyContract();
console.log(typeof instance.methods.foo) // output `function`
This function is designed to invoke the corresponding @method
of the same name on chain, meaning: calling it will spend the previous contract UTXO in a new transaction. You can call it like this:
// Note: `instance.methods.foo` should be passed in all arguments and in the same order that `instance.foo` would take.
// Additionally, it can accept an optional "options" argument to control the behavior of the function.
const { tx, atInputIndex } = await instance.methods.foo(arg1, arg2, options);
What actually happens during the call is the following:
- Build an unsigned transaction by calling the tx builder, which can be a default or a customized one introduced in this section, for a public
@method
. - Use the instance's signer to sign the transaction. Note that
instance.foo
could be invoked during this process in order to get a valid unlocking script for the input. - Use the instance's connected
provider
to send the transaction.
MethodCallOptions
The options
argument is of type MethodCallOptions
:
/**
* A option type to call a contract public `@method` function.
* Used to specify the behavior of signers and transaction builders.
* For example, specifying a transaction builder to use a specific change address or specifying a signer to use a specific public key to sign.
*/
export interface MethodCallOptions<T> {
/**
* The private key(s) associated with these address(es) or public key(s)
* must be used to sign the contract input,
* and the callback function will receive the results of the signatures as an argument named `sigResponses`
* */
readonly pubKeyOrAddrToSign?: PublicKeysOrAddressesOption | SignaturesOption;
/** The subsequent contract instance(s) produced in the outputs of the method calling tx in a stateful contract */
readonly next?: StatefulNext<T>[] | StatefulNext<T>,
/** The `lockTime` of the method calling tx */
readonly lockTime?: number;
/** The `sequence` of the input spending previous contract UTXO in the method calling tx */
readonly sequence?: number;
/** The P2PKH change output address */
readonly changeAddress?: AddressOption;
/** verify the input script before send transaction */
readonly verify?: boolean;
/** Whether to call multiple contracts at the same time in one transaction */
readonly multiContractCall?: true;
/** Pass the `ContractTransaction` of the previous call as an argument to the next call, only used if `multiContractCall = true`. */
readonly partialContractTx?: ContractTransaction;
}
The major differences between here and local tests are:
- the contract needs to be deployed first
- the contract instance is connected to a real provider, which broadcasts transactions to the blockchain.
next
The next
property within the MethodCallOptions
interface in sCrypt is used to specify the subsequent contract instance(s) produced in the outputs of the method calling transaction in a stateful contract. This property allows for the chaining of stateful contract calls within a single transaction.
The transaction builder uses the passed instance(s) to construct outputs of the contract call transaction.
When writing a custom transaction builder we can access the instance like:
static unlockTxBuilder(
current: Demo,
options: MethodCallOptions<Demo>,
...
): Promise<ContractTransaction> {
const next = options.next as StatefulNext<Demo>
...
}
Create a smart contract instance from a transaction
To interact with a deployed smart contract (i.e., calling its public methods), we need its contract instance corresponding to its latest state on chain, stateful or not. When testing on testnet, we usually put a contract's deployment and its calling (note there could be multiple calls if the contract is stateful) in the same process for convenience, so that we don't need to manage the internal state of the instance manually, because it's always consistent with the transactions on chain.
In a production context, a contract's deployment and its call, and its different calls in the case of a stateful contract, may well be in separate processes. For example, the deployment party is different from the calling party, or multiple parties call it. If so, we need to create a contract instance from an on-chain transaction that represents its latest state, before we can call its method.
Typically, we only know the TXID of the transaction containing the instance. We can create an instance in two steps:
- Using TXID, we retrieve the full transaction by calling getTransaction of the connected provider of the signer.
- We can create an contract instance from a transaction's by calling fromTx().
// 1) fetch a transaction from txid
const tx = await signer.connectedProvider.getTransaction(txId)
// 2) create instance from transaction
const instance = Counter.fromTx(tx, atOutputIndex)
// from now on, `instance` is in sync with the on-chain transaction
// and we can use it to interact with the contract
A complete example can be found here.
Method with Signatures
A contract public @method
often needs a signature argument for authentication. Take this Pay To PubKey Hash (P2PKH) contract for example:
export class P2PKH extends SmartContract {
@prop()
readonly address: Addr
constructor(address: Addr) {
super(..arguments)
this.address = address
}
@method()
public unlock(sig: Sig, pubkey: PubKey) {
// make sure the `pubkey` is the one locked with its address in the constructor
assert(pubKey2Addr(pubkey) == this.address, 'address check failed')
// make sure the `sig` is signed by the private key corresponding to the `pubkey`
assert(this.checkSig(sig, pubkey), 'signature check failed')
}
}
We can call the unlock
method like this:
// call
const { tx: callTx } = await p2pkh.methods.unlock(
// the first argument `sig` is replaced by a callback function which will return the needed signature
(sigResps) => findSig(sigResps, publicKey),
// the second argument is still the value of `pubkey`
PubKey(toHex(publicKey)),
// method call options
{
// A request for signer to sign with the private key corresponding to a public key
pubKeyOrAddrToSign: publicKey
} as MethodCallOptions<P2PKH>
);
console.log('contract called: ', callTx.id);
When p2pkh.method.unlock
is called, the option contains pubKeyOrAddrToSign
, requesting a signature against publicKey
.
The first argument is a signature, which can be obtained in a callback function. The function takes a list of signatures requested in pubKeyOrAddrToSign
and find the one signature to the right public key/address.
In general, if your @method
needs Sig
-typed arguments, you could obtain them as follows:
- Ensure that the
pubKeyOrAddrToSign
contains all public keys/addresses corresponding to theseSig
s; - Replace each
Sig
argument with a callback function that filters to the rightSig
from the full list of signature insigResps
.
Example
Here is the complete sample code for the deployment and call of a P2PKH contract.
import { privateKey } from '../../utils/privateKey';
// load contract
await P2PKH.loadArtifact()
// public key of the `privateKey`
const publicKey = privateKey.publicKey
// setup signer
const signer = new TestWallet(privateKey, new DefaultProvider());
// initialize an instance with `pkh`
let p2pkh = new P2PKH(Addr(publicKey.toAddress().toByteString()))
// connect the signer
await p2pkh.connect(signer);
// deploy the contract, with 1 satoshi locked in
const deployTx = await p2pkh.deploy(1);
console.log('contract deployed: ', deployTx.id);
// call
const { tx: callTx } = await p2pkh.methods.unlock(
(sigResps) => findSig(sigResps, publicKey),
PubKey(toHex(publicKey)),
{
pubKeyOrAddrToSign: publicKey
} as MethodCallOptions<P2PKH>
);
console.log('contract called: ', callTx.id);
More examples can be found here.
Running the code
The deployment and call code is wrapped into a simple NPM command:
npm run testnet
Make sure you fund your address before running this command. After a successful run you should see something like:
P2PKH contract deployed: f3f372aa25f159efa93db8c51a4eabbb15935358417ffbe91bfb78f4f0b1d2a3
P2PKH contract called: dc53da3e80aadcdefdedbeb6367bb8552e381e92b226ab1dc3dc9b3325d8a8ee
These are the TXIDs of the transaction which deployed the smart contract and then the one which called its method. You can see the transactions using a block explorer.
Customize Transactions
Deploying and calling a contract builds transactions with a certain format, which suffices for many cases. In cases where the tx format does not work for you and you need to customize it, please proceed to the next section.