Skip to main content

Multiplex Contracts

Multiplex contracts are collections of contracts with complex relationships. Members of multiplex contract collection can reference other contracts and inspect their properties, such as: template bytecode, templateHash, constraintHash, visibleParams and lockingBytecode (all accessors are of type bytes).

Contract syntax

Multiplex contracts enable a new feature to define several contracts within a single source file:

Multiplex.nex
pragma nexscript ^0.7.0;

contract A() {
function A(string vA, int x) {
bytes lockingBytecode = B.lockingBytecode;
require(lockingBytecode.length != 0);

require(vA == "A");
require(x == 1);
}

function B(string vA, int x) {
require(vA == "B");
require(x == 2);
}
}

contract B() {
function Bv(string vB) {
require(vB == "B");
}
}

contract C() {
function Cv(string vC) {
bytes20 hash = A.templateHash;
require(hash.length != 0);
require(vC == "C");
}
}

Note, how contracts A and C reference other contract's pseudo-properties - so called accessors.

tip
  • Contract names must be unique
  • There must be no circular dependencies in multiplex contracts

Multiplex contracts allow interactions with MCP contracts. Let us reuse the previous contract and add an MCP contract to it.

McpMultiplex.nex
pragma nexscript ^0.7.0;

contract A() {
...
}

contract B() {
...
}

contract C() {
...
}

// Define an MCP contract
contract MCP() {

contract MastA() {
function constraint(string vA, int x) {
require(vA == "A");
require(x == 1);
require(tx.outputs[0].lockingBytecode = A.lockingBytecode);
}
}

contract MastB() {
function constraint() {
require(tx.outputs[0].lockingBytecode = B.lockingBytecode);
}
}
}

contract McpCovenant() {
function covenantConstraint() {
require(tx.outputs[0].lockingBytecode = MCP.lockingBytecode);
}
}

Note, how we can reference contracts A and B from within MAST contracts. Also, a very powerful feature is that we support the accessors of the MCP contract itself (see McpCovenant contract).

tip

Also note, that if a top-level contract defines child contracts, it will be treated as an MCP contract and not as a convetional one.

Instantiation in SDK

From nexscript 0.7.0 multiplex contract artifacts are the standard one being output by the compiler.

To compile your contract string, use:

import { compileString } from '@nexscript/nexc';
const artifact = compileString(source);

or to compile from file:

import { compileFile } from '@nexscript/nexc';
const artifact = compileFile(path);

Then having an artifact, you can instantiate a contract from the source:

import {
ElectrumNetworkProvider,
Contract,
} from '@nexscript/nexscript';

const provider = new ElectrumNetworkProvider();
const contract = new ContractContract(artifact, [], { provider, contractName: "B" });

// send some satoshis to contract address in order to "deploy" it on chain
// await fund(contract.address, 10000);

await contract.Bv("B").to(aliceAddress, 1000n).send();

Note, how we specify the top-level contract name which has to be initialized: contractName: "B". If we would omit it, the first contract will be initialized implicitly: A.

Contract dependencies

It is important to note, that at the compilation time, only contract's template and templateHash accessors are known. constraintHash, visibleParams and lockingBytecode are properties of an already existing (deployed) contracts. So to execute any function of contract A we would need to know the creation params of contract B, or to execute any function of contract C, we would need to know the creation params of both the contract A and contract B.

Let us demonstrate the instantiation of contract C:

import { compileString } from '@nexscript/nexc';
const artifact = compileString(source);

const contract = new Contract(artifact, ['C'], {
provider,
contractName: 'C',
dependencyArgs: {
A: {
constructorInputs: artifact.contracts[0].constructorInputs,
constructorArgs: ['A', 1n],
},
B: {
constraintHash: Uint8Array.from([0x00]),
visibleArgs: Uint8Array.from([]),
lockingBytecode: Uint8Array.from([]),
},
},
});

Note, how we have two options to specify the contract creation params:

  • for contract A we use constructorInputs known from the artifact and constructor args are the same as we would have passed to contract A instantiation.
  • for Contract B we use another notation - full specification of the properties of an already deployed contract: constraintHash, visibleArgs, lockingBytecode.

And because the full specification of the properties of an already deployed contract (constraintHash, visibleArgs, lockingBytecode) is exactly the contract's address, there is a better way around for specifying the dependency params for B: using a utility function getContractCreationParamsFromAddress.

  dependencyArgs: {
...
B: getContractCreationParamsFromAddress("nexa:nqt...."),
},