The Gear-JS API provides a set of utilities, libraries and tools that enable JavaScript applications to interact with smart contracts running in the Gear-based networks via queries to a Gear node.
npm install @gear-js/api
or
yarn add @gear-js/api
Start an API connection to a running node on localhost:
import { GearApi } from '@gear-js/api';
const gearApi = await GearApi.create();
You can also connect to a different node:
const gearApi = await GearApi.create({ providerAddress: 'wss://someIP:somePort' });
Getting node info:
const chain = await gearApi.chain();
const nodeName = await gearApi.nodeName();
const nodeVersion = await gearApi.nodeVersion();
const genesis = gearApi.genesisHash.toHex();
GearApi
class to interact with the node, but it can be more convivnient to use classes based on a specific runtime version.
For example for interacting with the network that has runtime version more than 1010
you can use the VaraApiV1010
class.It's necessary to send only bytes to interact with programs on blockchain.
For that purpose we use the scale-codec
implementation from @polkadot-js
You can use static CreateType.create
method to encode and decode data with standart types
import { CreateType } from '@gear-js/api';
const result = CreateType.create('TypeName', somePayload);
The result of these functions is the data of type Codec
and it has the following methods:
result.toHex(); // - returns a hex represetation of the value
result.toHuman(); // - returns human friendly object representation of the value
result.toString(); // - returns a string represetation of the value
result.toU8a(); // - encodes the value as a Unit8Array
result.toJSON(); // - converts the value to JSON
There're 2 types of metadata.
ProgramMetadata
is used to encode/decode messages to/from a program as well as to read the whole state of the programYou can use ProgramMetadata.from
method to create the program metadata. The method takes metadata of the program in format of hex string. It will return object of ProgramMetadata
class that has property types
that contains all types of the program.
You should pass an object of this class to function arguments when you want to send some extrinsics that require encoding payloads
import { ProgramMetadata } from '@gear-js/api';
const meta = ProgramMetadata.from(`0x...`);
meta.types.init.input; // can be used to encode input message for init entrypoint of the program
meta.types.init.output; // can be used to decode output message for init entrypoint of the program
// the same thing available for all entrypoints of the program
meta.types.state; // contains type for decoding state output
StateMetadata
is used to encode input/decode output payloads that is needed to read state using a specific wasm.You can use getStateMetadata
function to create program metadata. The function takes wasm as Buffer to read state. It will return object of StateMetadata class that has property functions
that contains available function of wasm and input/output types
import { getStateMetadata } from '@gear-js/api';
const fileBuffer = fs.readFileSync('path/to/state.meta.wasm');
const meta = await getStateMetadata(fileBuffer);
meta.functions; // is an object whose keys are names of funtions and values are objects of input/output types
Both ProgramMetadata
and StateMetadata
classes have a few methods that can help to understand what some type is or get the name of some type (because types are represented as number in regestry) as well as encode and decode data.
import { ProgramMetadata } from '@gear-js/api';
const meta = ProgramMetadata.from(`0x...`);
meta.getTypeName(4); // will return name of type with this index
// or
meta.getTypeName(meta.types.handle.input);
meta.getTypeDef(4); // will return structure of this type
meta.getTypeDef(4, true); // if you need to get type structre with additional field (name, type, kind, len) you have to pass the second argument
meta.getAllTypes(); // will return all custom types existed in the registry of the program
meta.createType(4, { value: 'value' }); // to encode or decode data
Extrinsics are used to interact with programs running on blockchain networks powered by Gear Protocol
In all cases of sending extrinsics, there is no need to encode the payloads by yourself. It's sufficient to pass the program metadata obtained from the ProgramMetadata.from
method to the methods that creates extrinsics.
Use api.program.upload
method to create upload_program
extrinsic
const code = fs.readFileSync('path/to/program.wasm');
const program = {
code,
gasLimit: 1000000,
value: 1000,
initPayload: somePayload,
};
const { programId, codeId, salt, extrinsic } = gearApi.program.upload(program, meta);
await extrinsic.signAndSend(keyring, (event) => {
console.log(event.toHuman());
});
Use api.code.upload
method to create upload_code
extrinsic
const code = fs.readFileSync('path/to/program.opt.wasm');
const { codeHash } = await gearApi.code.upload(code);
gearApi.code.signAndSend(alice, () => {
events.forEach(({ event: { method, data } }) => {
if (method === 'ExtrinsicFailed') {
throw new Error(data.toString());
} else if (method === 'CodeChanged') {
console.log(data.toHuman());
}
});
});
Use api.program.create
method to create create_program
extrinsic
const codeId = '0x...';
const program = {
codeId,
gasLimit: 1000000,
value: 1000,
initPayload: somePayload,
};
const { programId, salt, extrinsic } = gearApi.program.create(program, meta);
await extrinsic.signAndSend(keyring, (event) => {
console.log(event.toHuman());
});
Use api.message.send
method to create send_message
extrinsic
try {
const message = {
destination: destination, // programId
payload: somePayload,
gasLimit: 10000000,
value: 1000,
keepAlive: true, // if set to true the account is protected against removal due to low balances.
};
// In that case payload will be encoded using meta.handle_input type
let extrinsic = await gearApi.message.send(message, meta);
// So if you want to use another type you can specify it
extrinsic = await gearApi.message.send(message, meta, meta.async_handle_input);
} catch (error) {
console.error(`${error.name}: ${error.message}`);
}
try {
await extrinsic.signAndSend(keyring, (event) => {
console.log(events.toHuman());
});
} catch (error) {
console.error(`${error.name}: ${error.message}`);
}
Use api.message.reply
method to create send_reply
extrinsic
const reply = {
replyToId: messageId,
payload: somePayload,
gasLimit: 10000000,
value: 1000,
keepAlive: true,
};
const extrinsic = await gearApi.message.sendReply(reply, meta);
await extrinsic(keyring, (events) => {
console.log(events.toHuman());
});
To get a transaction fee before sending a transaction you can use paymentInfo
method.
const api = await GearApi.create();
api.program.upload({ code, gasLimit });
// same for api.message, api.reply and others
const paymentInfo = await api.program.paymentInfo(alice);
const transactionFee = paymentInfo.partialFee.toNumber();
consolg.log(transactionFee);
To find out the minimum gas amount to send extrinsic, use gearApi.program.calculateGas.[method]
.
There are 4 methods to use:
init
- to calculate the gas of init message that will be applied to upload_program
extrinsicinitCreate
- to calculate the gas of init message that will be applied to create_program
extrinsichandle
- to calculate the gas of handle message that will be applied to send_message
extrinsicreply
- to calculate the gas of reply message that will be applied to send_reply
extrinsicGas calculation returns GasInfo object contains 5 parameters:
min_limit
- Minimum gas limit required for executionreserved
- Gas amount that will be reserved for some other on-chain interactionsburned
- Number of gas burned during message processingmay_be_returned
- value that can be returned in some caseswaited
- notifies that the message will be added to waitlistcalculateGas.handle
methodconst code = fs.readFileSync('demo_meta.opt.wasm');
const meta = ProgramMetadata.from('0x...');
const gas = await gearApi.program.calculateGas.handle(
'0x...', // source id
'0x...', //program id
{
id: {
decimal: 64,
hex: '0x...',
},
}, // payload
0, // value
false, // allow other panics
meta, // the metadata of the program
);
console.log(gas.toHuman());
To resume paused program use api.program.resumeSession
methods.
init
- To start new session to resume program
push
- To push a bunch of the program pages
commit
- To finish resume session
const program = await api.programStorage.getProgram(programId, oneBlockBeforePauseHash);
const initTx = api.program.resumeSession.init({
programId,
allocations: program.allocations,
codeHash: program.codeHash.toHex(),
});
let sessionId: HexString;
initTx.signAndSend(account, ({ events }) => {
events.forEach(({ event: { method, data }}) => {
if (method === 'ProgramResumeSessionStarted') {
sessionId = data.sessionId.toNumber();
}
})
})
const pages = await api.programStorage.getProgramPages(programId, program, oneBlockBeforePauseHash);
for (const memPage of Object.entries(page)) {
const tx = api.program.resumeSession.push({ sessionId, memoryPages: [memPage] });
tx.signAndSend(account);
}
const tx = api.program.resumeSession.commit({ sessionId, blockCount: 20_000 });
tx.signAndSend(account);
Use api.voucher.issue
method to issue a new voucher for a user to be used to pay for sending messages to programs.
import { VoucherIssued } from '@gear-js/api';
const programs = ['0x1234...', '0x5678...'];
const spenderAddress = '0x...';
const validForOneHour = (60 * 60) / 3; // number of blocks in one hour
const { extrinsic } = await api.voucher.issue(spenderAddress, 100 * 10 ** 12, validForOneHour, programs, true);
// To allow the voucher to be used for code uploading, set the last argument of the `.issue` method to true
extrinsic.signAndSend(account, ({ events }) => {
const voucherIssuedEvent = events.find(({event: { method }}) => method === 'VoucherIssued')?.event as VoucherIssued;
if (voucherIssuedEvent) {
console.log(voucherIssuedEvent.toJSON());
}
})
The api.voucher.exists
method returns a boolean value indicates whether the voucher exists or not.
const voucherExists = await api.voucher.exists(accountId, programId)
The api.voucher.getAllForAccount
method returns an object whose key is the voucher id and value is an array of programs for which the voucher can be used.
const allVouchers = await api.voucher.getAllForAccount(accountId);
const details = api.voucher.details(spenderAddress, voucherId);
console.log(`Voucher details:
owner: ${details.owner}
programs: ${details.programs}
expiry: ${details.expiry}`);
To send message with voucher you can use api.voucher.call
method.
const messageTx = api.message.send({
destination: destination,
payload: somePayload,
gasLimit: 10000000,
value: 1000
}, meta);
const voucherTx = api.voucher.call(voucherId, { SendMessage: messageTx });
await voucherTx.signAndSend(account, (events) => {
console.log(events.toHuman());
});
It works in the same way as sending message with voucher
const messageTx = api.message.sendReply(...);
const voucherTx = api.voucher.call(voucherId, { SendReply: messageTx });
await voucherTx.signAndSend(account, (events) => {
console.log(events.toHuman());
});
const { extrinsic } = await api.code.upload(code);
const tx = api.voucher.call(voucherId, { UploadCode: extrinsic })
await tx.signAndSend(account, (events) => {
console.log(events.toHuman());
});
The api.voucher.update
can be used to update the voucher. All parameters in the 3rd argument are optional, but at least one parameter must be specified.
const tx = await api.voucher.update(
spenderAddress,
voucherId,
{
moveOwnership: newOwnerAddress, // The new voucher owner
balanceTopUp: 1_000 * 10 ** 12, // Top up voucher balance
appendPrograms: ['0x9123...', '0x4567...'], // Append programs for which the voucher can be used
prolongValidity: 1_000_000 // Prolong the voucher validity for 1_000_000 blocks
}
)
The api.voucher.revoke
is used to revoke an issued voucher. It's possible to revoke a voucher only after the validity period has expired.
const tx = api.voucher.revoke(spenderAddress, voucherId);
tx.signAndSend(...);
The api.voucher.decline
can be used to decline existing and not expired voucher. It will make the voucher expired
const tx = api.voucher.decline(voucherId);
tx.signAndSend(...);
To find out if an address belongs to a program use the api.program.exists
method.
const programId = '0x...';
const programExists = await api.program.exists(programId);
console.log(`Program with address ${programId} ${programExists ? 'exists' : "doesn't exist"}`);
Since v1.2.0
release of the gear
pallet, it's possible to send a message to the program and get the reply without transaction.
The api.message.calculateReply
method can be used for that purpose.
const programId = '0x..';
const origin = '0x...'; // the address of the sender
const meta = ProgramMetadata.from('0x...');
const result = await api.message.calculateReply({
origin,
destination: programId,
payload: { myPayload: [] },
value: 0
}, meta);
console.log(result.toJSON());
console.log('reply payload:', meta.createType(meta.types.handle.output, result.payload).toJSON());
There are 2 ways to read state of a program.
Read full state of a program.
To read full state of the program you need to provide metadata of this program and input payload expected by the program. You can call api.programState.read
method to get the state.
await api.programState.read({ programId: `0x...`, payload: [1, 2, 3] }, programMetadata);
// Also you can read the state of the program at some specific block.
await api.programState.read({ programId: `0x...`, payload: [1, 2, 3], at: `0x...` }, programMetadata);
Read state using wasm.
If you have some program that is able to read state and return you only necessary data you need to use api.programState.readUsingWasm
method.
Note that since state
function of gear programs expects input payload you need to provide it in the parameters of readUsingWasm
method
const wasm = readFileSync('path/to/state.meta.wasm');
const stateMetadata = await getStateMetadata(wasm);
const programMetadata = ProgramMetadata.from('0x...');
const state = await api.programState.readUsingWasm(
{ programId, fn_name: 'name_of_function_to_execute', wasm, payload: [1, 2, 3], argument: { input: 'payload' } },
stateMetadata,
programMetadata
);
To get all ids of uploaded codes use api.code.all
method. It returns array of code ids
const codeIds = await gearApi.code.all();
console.log(codeIds);
The mailbox contains messages that are waiting for user action.
To read the mailbox use api.mailbox.read
method.
const api = await GearApi.create();
const mailbox = await api.mailbox.read('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
console.log(mailbox);
To claim value from a message in the mailbox use api.mailbox.claimValue.submit
method.
const api = await GearApi.create();
const submitted = await api.mailbox.claimValue.submit(messageId);
await api.mailbox.claimValue.signAndSend(...);
To read the program's waitlist use api.waitlist.read
method.
const gearApi = await GearApi.create();
const programId = '0x1234...';
const waitlist = await api.waitlist.read(programId);
console.log(waitlist);
const unsub = await gearApi.query.system.events((events) => {
console.log(events.toHuman());
});
// Unsubscribe
unsub();
gearApi.query.system.events((events) => {
events
.filter(({ event }) => gearApi.events.gear.MessageEnqueued.is(event))
.forEach(({ event: { data } }) => {
console.log(data.toHuman());
});
events
.filter(({ event }) => gearApi.events.balances.Transfer.is(event))
.forEach(({ event: { data } }) => {
console.log(data.toHuman());
});
});
There is an oportunity to subscribe to a specific event of the Gear pallet without filtering events manually.
const unsub = api.gearEvents.subscribeToGearEvent(
'UserMessageSent', // pass here name of event you're interested in
({
data: {
message: { id, source, destination, payload, value, reply },
},
}) => {
console.log(`
messageId: ${id.toHex()}
source: ${source.toHex()}
payload: ${payload.toHuman()}
`);
},
);
// Unsubscribe
unsub();
const data = await gearApi.blocks.get(blockNumberOrBlockHash);
console.log(data.toHuman());
const ts = await gearApi.blocks.getBlockTimestamp(blockNumberOrBlockHash);
console.log(ts.toNumber());
const hash = await gearApi.blocks.getBlockHash(blockNumber);
console.log(hash.toHex());
const hash = await gearApi.blocks.getBlockNumber(blockHash);
console.log(hash.toNumber());
const events = await gearApi.blocks.getEvents(blockHash);
events.forEach((event) => {
console.log(event.toHuman());
});
const extrinsics = await gearApi.blocks.getExtrinsics(blockHash);
extrinsics.forEach((extrinsic) => {
console.log(extrinsic.toHuman());
});
To create keyring you can use static methods of GearKeyring
class.
import { GearKeyring } from '@gear-js/api';
const { keyring, json } = await GearKeyring.create('keyringName', 'passphrase');
const jsonKeyring = fs.readFileSync('path/to/keyring.json').toString();
const keyring = GearKeyring.fromJson(jsonKeyring, 'passphrase');
const json = GearKeyring.toJson(keyring, 'passphrase');
const seed = '0x496f9222372eca011351630ad276c7d44768a593cecea73685299e06acef8c0a';
const keyring = await GearKeyring.fromSeed(seed, 'name');
const mnemonic = 'slim potato consider exchange shiver bitter drop carpet helmet unfair cotton eagle';
const keyring = GearKeyring.fromMnemonic(mnemonic, 'name');
const { mnemonic, seed } = GearKeyring.generateMnemonic();
// Getting a seed from mnemonic
const { seed } = GearKeyring.generateSeed(mnemonic);
import { GearKeyring } from '@gear-js/api';
const message = 'your message';
const signature = GearKeyring.sign(keyring, message);
import { signatureIsValid } from '@gear-js/api';
const publicKey = keyring.address;
const verified = signatureIsValid(publicKey, signature, message);
Use encodeAddress
and decodeAddress
functions to convert the public key into ss58 format and back.
import { decodeAddress } from '@gear-js/api';
console.log(decodeAddress('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'));
import { encodeAddress } from '@gear-js/api';
console.log(encodeAddress('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'));