Skip to main content

Smart Contracts

Smart contracts allow you to manage the state of the blockchain through code deployed in the blockchain. They live on the blockchain permanently, execute on the ULVM — ULedger's WebAssembly virtual machine — and every invocation is itself a transaction on chain.

Smart contracts are written in WebAssembly and compiled into .wat files. If you haven't written one yet, start with the ULVM documentation. It covers the basics on writing Smart contracts.


Deploying a contract

Deployment is a multipart POST. You send the .wat file alongside the transaction parameters. The wallet you deploy with must already be registered on the blockchain.

import fs from 'fs';
import FormData from 'form-data';

const form = new FormData();
form.append('walletAddress', 'your-wallet-address');
form.append('blockchainId', '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2');
form.append('from', 'your-wallet-address');
form.append('payload', 'erc20-token-contract');
form.append('file', fs.createReadStream('./contracts/erc20.wat'));

const res = await fetch('https://your-tms-url/api/v1/smartcontracts/deploy', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
...form.getHeaders(),
},
body: form,
});

const deployResult = await res.json();
console.log('Deploy transaction ID:', deployResult.transactionId);

The deploy creates a transaction on chain. That to address is also your contract address. Hold on to it to invoke the contract later. You should be able to see the transaction on a new block immediately after deploying.


Invoking a contract

Call any exported function by name and its arguments. The contract executes on the ULVM and every invocation is a transaction on chain. You can check the transaction status, the block it landed in, and the contract state after invoking.

const res = await fetch('https://your-tms-url/api/v1/smartcontracts/invoke/payload', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
walletAddress: 'your-wallet-address',
blockchainId: '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2',
contractAddress: deployResult.transactionId,
functionName: 'mint',
arguments: [
{ type: 'string', value: 'your-wallet-address' },
{ type: 'int32', value: '1000' },
],
gasLimit: 0,
}),
});

const invokeResult = await res.json();
console.log('Invocation result:', invokeResult);

Supported argument types: int32, int64, float32, float64, bool, string, bytes.


Checking contract state

Smart contracts can hold state. After invoking, check what changed by calling the node directly:

const stateRes = await fetch(
`${process.env.NODE_URL}/blockchains/${blockchainId}/contracts/${contractAddress}/state`,
{ headers: { 'Accept': 'application/json' } }
);

const state = await stateRes.json();
console.log('Contract state:', state);

If your contract emits logs you can trace a specific invocation:

const traceRes = await fetch(
`${process.env.NODE_URL}/blockchains/${blockchainId}/contracts/${contractAddress}/trace/${transactionId}`,
{ headers: { 'Accept': 'application/json' } }
);

const trace = await traceRes.json();
console.log('Trace:', trace);

Upgrading a contract

The contract address stays the same across upgrades — only the code changes. Every previous version is preserved on chain.

import fs from 'fs';
import FormData from 'form-data';

const form = new FormData();
form.append('walletAddress', 'your-wallet-address');
form.append('blockchainId', '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2');
form.append('contractAddress', deployResult.transactionId);
form.append('upgradeReason', 'added burn function');
form.append('file', fs.createReadStream('./contracts/erc20-v2.wat'));

const upgradeRes = await fetch('https://your-tms-url/api/v1/smartcontracts/upgrade', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
...form.getHeaders(),
},
body: form,
});

console.log('Upgrade status:', upgradeRes.status);

After the upgrade, invoke new functions the same way — same contract address, new function names available:

const res = await fetch('https://your-tms-url/api/v1/smartcontracts/invoke/payload', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
walletAddress: 'your-wallet-address',
blockchainId: '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2',
contractAddress: deployResult.transactionId,
functionName: 'burn',
arguments: [
{ type: 'string', value: 'your-wallet-address' },
{ type: 'int32', value: '100' },
],
gasLimit: 0,
}),
});

Deploy once, invoke as many times as needed, upgrade when the logic changes. The contract is always there, always on chain, always traceable.