Contract Instantiation
Before interacting with smart contracts on the Nexa network, the NexScript SDK needs to instantiate a Contract object. This is done by providing the contract's information and constructor arguments. After this instantiation, the NexScript SDK can interact with Nexa contracts.
Contract class
The Contract class is used to represent a NexScript contract in a JavaScript object. These objects can be used to retrieve information such as the contract's address and balance. They can be used to interact with the contract by calling the contract's functions.
Constructor
new Contract(
artifact: SourceArtifact | ContractArtifact,
constructorArgs: Argument[],
options?: ContractOptions,
)
interface ContractOptions {
provider?: NetworkProvider,
contractName?: string,
dependencyArgs?: ContractDependencies,
}
A NexScript contract can be instantiated by providing an artifact, a list of constructor arguments, and optionally a ContractOptions object.
The artifact is the result of compiling a NexScript source file: a SourceArtifact holding every contract in the file, or a single ContractArtifact from its contracts list. See the Language Documentation for more information on Artifacts. Compilation can be done using the standalone nexc CLI or programmatically with the @nexscript/nexc NPM package (see NexScript Compiler). When a SourceArtifact contains more than one contract, contractName selects the contract to instantiate; without it the first contract is used. dependencyArgs provides the creation parameters of contracts this contract references, see Multiplex Contracts.
A NetworkProvider is used to manage network operations for the NexScript contract. By default, an ElectrumNetworkProvider is used for the network set in the NETWORK environment variable (mainnet or testnet), or for regtest when it is not set. Alternative network providers can be used. See the section on NetworkProvider below.
Example
import { Contract, ElectrumNetworkProvider } from '@nexscript/nexscript';
import { compileFile } from '@nexscript/nexc';
// Compile a contract file (or import the artifact JSON written by the nexc CLI)
const P2PKT = compileFile(new URL('p2pk.nex', import.meta.url));
const provider = new ElectrumNetworkProvider('testnet');
const contract = new Contract(P2PKT, [alicePk], { provider });
address
contract.address: string
A contract's address can be retrieved through the address member field.
Example
console.log(contract.address)
tokenAddress()
contract.tokenAddress(groupId: string, amount: bigint): string
Returns the address of an output that locks amount of the token groupId (hex) to the contract.
tokenAddress() always returns a mainnet (nexa:) address, even when the contract's provider is for testnet or regtest. This is a known issue.
Example
console.log(contract.tokenAddress(groupId, 1000n))
opcount
contract.opcount: number
The op count the node will reach running the contract can be retrieved through the opcount member field: every opcode above OP_16, including those in branches that do not run, plus the key count of each checkMultiSig. A Nexa script template can count at most 50,000 ops; the compiler rejects contracts over this limit.
Example
assert(contract.opcount <= 50000)
bytesize
contract.bytesize: number
The size of the contract's bytecode in bytes can be retrieved through the bytesize member field. A Nexa script template can be at most 100,000 bytes; the compiler rejects contracts over this limit.
Example
console.log(contract.bytesize)
bytecode
contract.bytecode: string
Returns the contract's template script encoded as a hex string. The constructor arguments are not part of it.
Example
console.log(contract.bytecode)
getBalance()
async contract.getBalance(): Promise<bigint>
Returns the total balance of the contract in satoshis. Both confirmed and unconfirmed balance is included in this figure.
Example
const contractBalance = await contract.getBalance()
getUtxos()
async contract.getUtxos(): Promise<Utxo[]>
Returns all UTXOs that can be spent by the contract. Both confirmed and unconfirmed UTXOs are included. On Nexa a UTXO is referenced by its outpoint hash, which is what the txid field holds.
interface Utxo {
txid: string;
vout: number;
satoshis: bigint;
address: string;
token?: TokenDetails;
}
Example
const utxos = await contract.getUtxos()
Contract functions
contract.functions.<functionName>(...args: Argument[]): Transaction
The main way to use smart contracts once they have been instantiated is through the functions defined in the NexScript source code. These functions can be found by their name under functions member field of a contract object. To call these functions, the parameters need to match ones defined in the NexScript code.
These contract functions return an incomplete Transaction object, which needs to be completed by providing outputs of the transaction. More information about sending transactions is found on the Sending Transactions page.
Example
import { alice } from './somewhere';
const tx = await contract.functions
.transfer(new SignatureTemplate(alice))
.to('nexa:nqtsq5g537fcf6z85pgwk4my5e5ddmypa2sm47mkzavt6zky', 10000n)
.send()
Contract unlockers
contract.unlock.<functionName>(...args: Argument[]): Unlocker
Each contract function is also available under the unlock member field. Instead of a Transaction, it returns an Unlocker that spends a contract UTXO through that function. Unlockers are passed to the Advanced Transaction Builder to combine inputs from several contracts and P2PKT addresses in one transaction. The arguments are the same as for contract.functions.
Example
const contractUtxos = await contract.getUtxos();
const transactionBuilder = new TransactionBuilder({ provider })
.addInput(contractUtxos[0], contract.unlock.transfer(new SignatureTemplate(alice)));
SignatureTemplate
new SignatureTemplate(
signer: PrivateKey | string,
hashtype?: HashType,
signatureAlgorithm?: SignatureAlgorithm,
network?: Network,
)
You may notice the SignatureTemplate object in the example above. When a contract function has a sig parameter, it requires a cryptographic signature over the spending transaction. But to generate this signature, the transaction needs to be built first, which is not yet the case when a contract function is first called.
So in the place of a signature, a SignatureTemplate can be passed, which will automatically generate the correct signature using the signer parameter. This signer can be a libnexa-ts PrivateKey or a WIF string. A WIF string is decoded for network, a libnexa-ts Network that defaults to Networks.mainnet. For a testnet or regtest key, pass Networks.testnet (regtest keys use the testnet WIF format), or pass a PrivateKey created with PrivateKey.fromWIF(wif), which detects the network.
hashtype and signatureAlgorithm are stored but not used when signing transactions: the SDK always signs with SIGHASH_ALL. This is a known limitation.
Example
const wif = '6FubvHFuHZ9D72vRTwELAEMBz3EBw4FiBtYAKMYi5TizN5G7TRor';
const sig = new SignatureTemplate(wif, HashType.SIGHASH_ALL);
getPublicKey()
signatureTemplate.getPublicKey(): PublicKey
Returns the libnexa-ts PublicKey of the signer, for example to pass as a pubkey constructor argument.
Example
const alicePk = aliceTemplate.getPublicKey().toBuffer();
unlockP2PKT()
signatureTemplate.unlockP2PKT(): Unlocker
Returns an Unlocker that spends a P2PKT UTXO of the signer's address. It is used with the Advanced Transaction Builder.
Example
const aliceUtxos = await provider.getUtxos(aliceAddress);
transactionBuilder.addInput(aliceUtxos[0], aliceTemplate.unlockP2PKT());
NetworkProvider
The NexScript SDK needs to connect to the Nexa network to perform certain operations, like retrieving the contract's balance, or sending transactions. All network functionality that the NexScript SDK needs is encapsulated in a network provider. This allows different network providers to be used and makes it easy to swap out dependencies.
ElectrumNetworkProvider
new ElectrumNetworkProvider(
network?: Network,
electrum?: ElectrumCluster,
manualConnectionManagement?: boolean,
)
The ElectrumNetworkProvider uses electrum-cash to connect to the Nexa network. This is the recommended provider for most use cases and is used as the default when no other provider is provided. All parameters are optional. network defaults to the NETWORK environment variable (mainnet or testnet), or to regtest when it is not set. Without a custom electrum cluster, the provider connects to a single Electrum server for mainnet, testnet or regtest (127.0.0.1:30403), and throws for any other network.
By default the provider connects before and disconnects after each request. Set manualConnectionManagement to true to keep one connection open, managed with connectCluster() and disconnectCluster().
Example
const provider = new ElectrumNetworkProvider('testnet');
getConfirmedBalance()
async provider.getConfirmedBalance(address: string): Promise<number>
Returns the confirmed balance of an address in satoshis, as reported by the Electrum server. Unconfirmed transactions are not included.
Example
const balance = await provider.getConfirmedBalance(aliceAddress);
connectCluster()
async provider.connectCluster(): Promise<void[]>
Connects to the Electrum server (or starts the custom cluster). Only needed with manualConnectionManagement. If the connection fails, it returns an empty array instead of throwing.
disconnectCluster()
async provider.disconnectCluster(): Promise<boolean[]>
Disconnects from the Electrum server (or shuts down the custom cluster).
Example
const provider = new ElectrumNetworkProvider('testnet', undefined, true);
await provider.connectCluster();
const utxos = await provider.getUtxos(aliceAddress);
const height = await provider.getBlockHeight();
await provider.disconnectCluster();
Custom NetworkProviders
A big strength of the NetworkProvider setup is that it allows you to implement custom providers. So if new Nexa libraries are created in the future, it is simple to use them with NexScript. This also potentially enables the NexScript SDK to be used with other (partially) compatible networks, such as BTC or BCH.
NetworkProvider interface
interface NetworkProvider {
/**
* Variable indicating the network that this provider connects to.
*/
network: Network;
/**
* Retrieve all UTXOs (confirmed and unconfirmed) for a given address.
* @param address The Nexa address for which we wish to retrieve UTXOs.
* @returns List of UTXOs spendable by the provided address.
*/
getUtxos(address: string): Promise<Utxo[]>;
/**
* @returns The current block height.
*/
getBlockHeight(): Promise<number>;
/**
* Retrieve the Hex transaction details for a given transaction ID.
* @param txid Hex transaction ID.
* @throws {Error} If the transaction does not exist
* @returns The full hex transaction for the provided transaction ID.
*/
getRawTransaction(txid: string): Promise<string>;
/**
* Broadcast a raw hex transaction to the network.
* @param txHex The raw transaction hex to be broadcast.
* @throws {Error} If the transaction was not accepted by the network.
* @returns The transaction ID corresponding to the broadcast transaction.
*/
sendRawTransaction(txHex: string): Promise<string>;
}
type Network = 'mainnet' | 'testnet3' | 'testnet4' | 'chipnet' | 'testnet' | 'staging' | 'regtest';
interface Utxo {
txid: string;
vout: number;
satoshis: bigint;
address: string;
token?: TokenDetails;
}
export interface TokenDetails {
amount: bigint;
groupId: string;
}
NexScript Compiler
Generally NexScript contracts are compiled to an Artifact JSON file using the CLI compiler. As an alternative to this, NexScript contracts can be compiled from within JavaScript apps using the @nexscript/nexc package. This package needs to be installed separately and exports two compilation functions.
npm install @nexscript/nexc
compileFile()
compileFile(sourceFile: PathLike): SourceArtifact
Compiles a NexScript contract from a source file. This is the recommended compile method if you're using Node.js and you have a source file available.
Example
const P2PKT = compileFile(new URL('p2pk.nex', import.meta.url));
compileString()
compileString(sourceCode: string): SourceArtifact
Compiles a NexScript contract from a source code string. This is the recommended compile method if you're building a webapp, because compileFile() only works from a Node.js context. This is also the recommended method if no source file is locally available (e.g. the source code is retrieved with a REST API).
const baseUrl = 'https://gitlab.com/nexa/nexscript/-/raw'
const result = await fetch(`${baseUrl}/master/examples/p2pk.nex`);
const source = await result.text();
const P2PKT = compileString(source);