TS Library
Now that your testnet RPC server is up, let's interact with your contract using the ts-library. The concept is similar to qubic-cli
: we craft the packet and send it to the node—but this time, we'll use code instead. However, a current limitation is that we still need to represent input
and output
as byte arrays in JavaScript and manually parse the response bytes to construct the output
. Let’s see how this works!
Concept
Any packet sent to the Qubic network must begin with a RequestResponseHeader
, which describes metadata such as the packet type and its total size. Similarly, responses from a Qubic node will also start with a RequestResponseHeader
.
The QubicPackageBuilder
class is a helper for combining the RequestResponseHeader
with other structures.
Each RequestResponseHeader
must include a unique dejavu.
Calling Function
Show getStateNumber
Contract Function
struct getStateNumber_input
{
};
struct getStateNumber_output
{
uint64 stateNumber;
};
PUBLIC_FUNCTION(getStateNumber)
{
output.stateNumber = state.stateNumber;
}
Instead of manually crafting and sending the packet, the RPC server provides a convenient API at /v1/querySmartContract
which allows us to make a POST
request to invoke a function and receive a result. Let's see how to call the getStateNumber
contract function described above.
The input
(body) and output
(response) of /v1/querySmartContract
must be encoded in base64 format.
Using RPC Server
async function main() {
const API = `http://ip/v1/querySmartContract`;
let responseBase64 = await fetch(API, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
contractIndex: 13,
inputType: 14, // function id
inputSize: 0,
requestData: "", // base64 encoded input data if any
}),
}).then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
});
// Decode the base64 response
// Struct output {
// uint64 stateNumber;
// }
const responseBuffer = Buffer.from(responseBase64, "base64");
const view = new DataView(responseBuffer.buffer);
const stateNumber = view.getBigUint64(0, true);
console.log("State Number:", stateNumber.toString());
}
main();
Directly To Node
You can also use ts-library
to send raw contract function packets directly to the node.
Show Code
async function main() {
const peerAddress = '162.120.19.25';
const connector = new QubicConnectorNode(31841);
if (!connector) {
console.error('Failed to create QubicConnectorNode instance.');
return;
}
connector.connect(peerAddress);
connector.onPeerConnected = () => {
console.log('Connected to peer:', peerAddress);
// First craft the RequestContractFunction package
// This is a request to the contract at index 13, function ID 14 with no input (INPUT_SIZE = 0)
const CONTRACT_INDEX = 13;
const FUNCTION_ID = 14;
const INPUT_SIZE = 0;
const request = new RequestContractFunction(
CONTRACT_INDEX,
FUNCTION_ID,
INPUT_SIZE
);
// craft the RequestResponseHeader package
const header = new RequestResponseHeader(
QubicPackageType.RequestContractFunction,
request.getPackageSize()
);
header.randomizeDejaVu();
// Now combine the header and request into a single package using QubicPackageBuilder
const builder = new QubicPackageBuilder(header.getSize());
builder.add(header);
builder.add(request);
const data = builder.getData();
connector.sendPackage(data);
console.log(
"Called contract function with ID",
FUNCTION_ID,
"on contract at index",
CONTRACT_INDEX,
"with input size",
INPUT_SIZE,
"to peer",
peerAddress
);
};
connector.onPackageReceived = (packageData) => {
if (
packageData.header.getType() !==
QubicPackageType.RespondContractFunction
) {
return;
}
let view = new DataView(packageData.payLoad.buffer);
let statateNumber = view.getBigUint64(0, true);
console.log('State Number: ', statateNumber.toString());
};
}
main();
Invoke Procedure
As discussed earlier, invoking a procedure is essentially sending a transaction. The ts-library
provides classes to help build these transactions. Let’s explore how it works.
Show setStateNumber
Contract Function
struct setStateNumber_input
{
uint64 stateNumber;
};
struct setStateNumber_output
{
uint8 result;
};
PUBLIC_PROCEDURE(setStateNumber)
{
if (input.stateNumber < state.stateNumber)
{
output.result = 1;
return;
}
state.stateNumber = input.stateNumber;
output.result = 0;
}
Using RPC Server
async function main() {
const BASE_URL = `http://162.120.18.27:8000/v1`;
// Will be used to set the transaction target tick
const getCurrentTick = async (): Promise<number> => {
const response = await fetch(`${BASE_URL}/tick-info`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let json = await response.json();
return json.tickInfo.tick;
};
let helper = new QubicHelper();
const seed = 'ghromhommngqxjokdlnyjkaoxmjbnwqneiikevfkxfncftudczluvcl';
const publicKey = new PublicKey(
(await helper.createIdPackage(seed)).publicKey
);
const CONTRACT_ADDRESS =
'NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAML';
const SetStateNumberProcedure = 15; // The procedure ID for SetStateNumber
// Create the payload buffer
const payloadBuffer = new Uint8Array(8);
const view = new DataView(payloadBuffer.buffer);
const stateNumber = 2n; // Example state number to set
view.setBigUint64(0, stateNumber, true); // Set the state number in the payload
// Then wrap it to DynamicPayload
let payLoad = new DynamicPayload(payloadBuffer.length);
payLoad.setPayload(payloadBuffer);
// Build the transaction
const tx = new QubicTransaction()
.setSourcePublicKey(publicKey)
.setDestinationPublicKey(CONTRACT_ADDRESS) // A transfer should go to the CONTRACT_ADDRESS
.setAmount(0) // SetStateNumber does not require any fee or cost
.setTick((await getCurrentTick()) + 10) // Set the target tick
.setInputType(SetStateNumberProcedure)
.setPayload(payLoad); // The payload contains the state number;
// Sign the tx
let txBuffer = await tx.build(seed);
// Convert to base64
let txBase64 = Buffer.from(txBuffer).toString('base64');
console.log('Transaction Base64:', txBase64);
// Then now broadcast the transaction to the network
const API = `${BASE_URL}/broadcast-transaction`;
let response = await fetch(API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
encodedTransaction: txBase64,
}),
});
response = await response.json();
console.log('Transaction broadcast response:', response);
console.log("Target tick:", tx.tick);
}
main();
Directly To Node
In case the RPC server is down, you can broadcast your transaction directly to the node.
Show Code
async function main() {
const peerAddress = "162.120.18.27";
const BASE_URL = `http://162.120.18.27:8000/v1`;
const getCurrentTick = async (): Promise<number> => {
const response = await fetch(`${BASE_URL}/tick-info`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let json = await response.json();
return json.tickInfo.tick;
};
let helper = new QubicHelper();
const seed = "ghromhommngqxjokdlnyjkaoxmjbnwqneiikevfkxfncftudczluvcl";
const publicKey = new PublicKey(
(await helper.createIdPackage(seed)).publicKey
);
const CONTRACT_ADDRESS =
"NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAML";
const SetStateNumberProcedure = 15; // The procedure ID for SetStateNumber
const payloadBuffer = new Uint8Array(8);
const view = new DataView(payloadBuffer.buffer);
const stateNumber = 3n; // Example state number to set
view.setBigUint64(0, stateNumber, true); // Set the state number in the payload
let payLoad = new DynamicPayload(payloadBuffer.length);
payLoad.setPayload(payloadBuffer);
const tx = new QubicTransaction()
.setSourcePublicKey(publicKey)
.setDestinationPublicKey(CONTRACT_ADDRESS) // A transfer should go to the CONTRACT_ADDRESS
.setAmount(0) // SetStateNumber does not require any fee or cost
.setTick((await getCurrentTick()) + 10) // Set the target tick
.setInputType(SetStateNumberProcedure)
.setPayload(payLoad); // The payload contains the state number;
let txBuffer = await tx.build(seed);
const header = new RequestResponseHeader(
QubicPackageType.BROADCAST_TRANSACTION,
tx.getPackageSize()
);
header.randomizeDejaVu();
const builder = new QubicPackageBuilder(header.getSize());
builder.add(header);
builder.addRaw(txBuffer);
const data = builder.getData();
let connector = new QubicConnectorNode(31841);
connector.connect(peerAddress);
connector.onPeerConnected = () => {
if (connector.sendPackage(data)) {
console.log("Transaction sent successfully to the node.");
console.log("Target tick:", tx.tick);
}
};
}
main();
References
For me details about ts-libray
please see https://github.com/qubic/ts-library