Load an ABI
Load a contract's ABI (Application Binary Interface), the description of its endpoints, custom types, and events, three ways: from a local file, from a URL, and by hand when no ABI file exists at all. This is the prerequisite every other recipe in this category builds on: Call a contract endpoint with native JS args, Query a read-only view, and Call a payable endpoint with EGLD all load an ABI first, the same way.
This recipe never sends a transaction or calls a devnet API. Loading and inspecting an ABI is entirely local, plus one plain HTTPS fetch for the "from a URL" case.
Prerequisites
- Node.js >= 20.19.0.
- Network access to fetch one ABI JSON file over HTTPS.
Install
mkdir load-abi
cd load-abi
# Create the project files shown on this page.
npm install
npm run build
npm start
Complete project files omitted from the main walkthrough
{
"name": "cookbook-recipe-load-abi",
"version": "1.0.0",
"private": true,
"description": "Cookbook recipe — load a contract ABI three ways (local file, URL, manual construct) with sdk-core's Abi/AbiRegistry, then introspect its endpoints. No wallet, no network write.",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit --strict"
},
"dependencies": {
"@multiversx/sdk-core": "15.4.1",
"axios": "1.18.1",
"bignumber.js": "9.3.1",
"protobufjs": "7.6.5"
},
"devDependencies": {
"@types/node": "20.19.43",
"typescript": "5.9.3"
},
"engines": {
"node": ">=20.19.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": false,
"sourceMap": false,
"outDir": "dist",
"types": ["node"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
{
"buildInfo": {
"rustc": {
"version": "1.76.0-nightly",
"commitHash": "d86d65bbc19b928387f68427fcc3a0da498d8a19",
"commitDate": "2023-12-10",
"channel": "Nightly",
"short": "rustc 1.76.0-nightly (d86d65bbc 2023-12-10)"
},
"contractCrate": {
"name": "adder",
"version": "0.0.0",
"gitVersion": "v0.50.1-3-gbed74682a"
},
"framework": {
"name": "multiversx-sc",
"version": "0.50.1"
}
},
"docs": [
"One of the simplest smart contracts possible,",
"it holds a single variable in storage, which anyone can increment."
],
"name": "Adder",
"constructor": {
"inputs": [
{
"name": "initial_value",
"type": "BigUint"
}
],
"outputs": []
},
"upgradeConstructor": {
"inputs": [
{
"name": "initial_value",
"type": "BigUint"
}
],
"outputs": []
},
"endpoints": [
{
"name": "getSum",
"mutability": "readonly",
"inputs": [],
"outputs": [
{
"type": "BigUint"
}
]
},
{
"docs": [
"Add desired amount to the storage variable."
],
"name": "add",
"mutability": "mutable",
"inputs": [
{
"name": "value",
"type": "BigUint"
}
],
"outputs": []
}
],
"esdtAttributes": [],
"hasCallback": false,
"types": {}
}
Loading
// src/loadAbi.ts — three ways to load a contract's ABI, plus the lookup
// methods each one gives you afterward.
//
// A naming subtlety worth being explicit about: `AbiRegistry` is the class
// with the real implementation — a protected constructor plus the static
// `create()` factory used below. `Abi` is a subclass that adds its own public
// constructor (taking already-built `EndpointDefinition`/`CustomType` objects,
// not raw JSON, which is not what you want for loading a JSON file) but does
// NOT override `create()`, so it inherits AbiRegistry's version as-is.
// `create()` always does `new AbiRegistry(...)` internally, regardless of
// which class name you called it through — so `Abi.create(json)` does not
// actually give you an `Abi` instance despite the name; both `Abi.create(json)`
// and `AbiRegistry.create(json)` return the exact same `AbiRegistry` instance
// (this is also the declared TypeScript return type of `create()` on both
// classes). The mx-sdk-js-core cookbook uses `Abi.create(...)`; mx-template-dapp's
// shipped widgets use `AbiRegistry.create(...)`. Both are correct and
// interchangeable — this recipe uses `Abi.create(...)`.
import * as fs from 'fs';
import axios from 'axios';
import { Abi } from '@multiversx/sdk-core';
import type { AbiRegistry } from '@multiversx/sdk-core';
/**
* Loads a contract's ABI from a local JSON file.
*/
export function loadAbiFromFile(filePath: string): AbiRegistry {
const json = fs.readFileSync(filePath, { encoding: 'utf8' });
return Abi.create(JSON.parse(json) as Record<string, unknown>);
}
/**
* Loads a contract's ABI from a URL — e.g. straight from a GitHub raw content
* URL, the same source this recipe's own src/adder.abi.json was copied from.
*/
export async function loadAbiFromUrl(url: string): Promise<AbiRegistry> {
const response = await axios.get<Record<string, unknown>>(url);
return Abi.create(response.data);
}
/**
* Manually constructs an ABI when no ABI file is available but the endpoint
* names and argument types are known. `foo` and `bar` are illustrative names
* only; this does not correspond to any real deployed contract.
*/
export function buildAbiManually(): AbiRegistry {
return Abi.create({
endpoints: [
{
name: 'foo',
inputs: [{ type: 'BigUint' }, { type: 'u32' }, { type: 'Address' }],
outputs: [{ type: 'u32' }],
},
{
name: 'bar',
inputs: [{ type: 'counted-variadic<utf-8 string>' }, { type: 'variadic<u64>' }],
outputs: [],
},
],
});
}
export interface EndpointSummary {
name: string;
mutability: string;
inputs: string[];
outputs: string[];
}
/**
* Summarizes every endpoint an ABI declares: name, mutability (readonly vs
* mutable, from `EndpointModifiers.isReadonly()`), and each parameter's type
* name — using `abi.getEndpoints()` and `EndpointDefinition.input` / `.output`.
* Those field names are singular, each an array; the ABI JSON itself uses the
* plural `inputs`/`outputs`, but the parsed `EndpointDefinition` object does not.
*/
export function summarizeEndpoints(abi: AbiRegistry): EndpointSummary[] {
return abi.getEndpoints().map((endpoint) => ({
name: endpoint.name,
mutability: endpoint.modifiers.isReadonly() ? 'readonly' : 'mutable',
inputs: endpoint.input.map((param) => `${param.name}: ${param.type.getName()}`),
outputs: endpoint.output.map((param) => param.type.getName()),
}));
}
Run it
// src/index.ts — CLI entry point. Loads the same ABI three ways and prints its
// endpoint summary each time (should be identical for the file/URL cases —
// same JSON, two sources), then shows the manual-construct path with its own
// illustrative endpoints.
//
// Usage:
// npm run build && npm start
//
// No wallet, no PEM, no network write — this recipe only reads a JSON file and
// (for the URL case) fetches one over HTTPS. The recipe bundles src/adder.abi.json.
import * as path from 'path';
import { loadAbiFromFile, loadAbiFromUrl, buildAbiManually, summarizeEndpoints } from './loadAbi';
const ADDER_ABI_URL =
'https://raw.githubusercontent.com/multiversx/mx-sdk-js-core/main/src/testdata/adder.abi.json';
async function main(): Promise<void> {
console.log('--- Loading from a local file (src/adder.abi.json) ---');
const fromFile = loadAbiFromFile(path.join(__dirname, '..', 'src', 'adder.abi.json'));
console.log(JSON.stringify(summarizeEndpoints(fromFile), null, 2));
console.log(`\n--- Loading the same ABI from a URL (${ADDER_ABI_URL}) ---`);
const fromUrl = await loadAbiFromUrl(ADDER_ABI_URL);
console.log(JSON.stringify(summarizeEndpoints(fromUrl), null, 2));
console.log('\n--- Manually constructed ABI (no file, illustrative "foo"/"bar" only) ---');
const manual = buildAbiManually();
console.log(JSON.stringify(summarizeEndpoints(manual), null, 2));
console.log('\n--- Single-endpoint lookup: abi.getEndpoint("add") ---');
const addEndpoint = fromFile.getEndpoint('add');
console.log(`add(${addEndpoint.input.map((p) => `${p.name}: ${p.type.getName()}`).join(', ')})`);
}
main().catch((err: unknown) => {
console.error(err);
process.exitCode = 1;
});
Expected output (abridged):
--- Loading from a local file (src/adder.abi.json) ---
[
{ "name": "getSum", "mutability": "readonly", "inputs": [], "outputs": ["BigUint"] },
{ "name": "add", "mutability": "mutable", "inputs": ["value: BigUint"], "outputs": [] }
]
--- Loading the same ABI from a URL (...) ---
[ ...identical output... ]
--- Manually constructed ABI (no file, illustrative "foo"/"bar" only) ---
[
{ "name": "foo", "mutability": "mutable", "inputs": ["?: BigUint", "?: u32", "?: Address"], "outputs": ["u32"] },
{ "name": "bar", "mutability": "mutable", "inputs": ["?: Variadic", "?: Variadic"], "outputs": [] }
]
--- Single-endpoint lookup: abi.getEndpoint("add") ---
add(value: BigUint)
How it works
Abi.create(json) and AbiRegistry.create(json) are the same inherited
method. AbiRegistry is the class with the real implementation: a protected
constructor plus the static create() factory. Abi extends AbiRegistry and
adds its own public constructor (for already-built EndpointDefinition /
CustomType objects, not raw JSON), but it does not override create(), which
always runs new AbiRegistry(...) internally. So Abi.create(json) does not
actually give you an Abi instance; both spellings return the same
AbiRegistry instance. Both work identically; this recipe follows the
Abi.create(...) naming.
src/adder.abi.json is the real ABI of a real, currently-deployed devnet
contract, erd1qqqqqqqqqqqqqpgq7cmfueefdqkjsnnjnwydw902v8pwjqy3d8ssd4meug,
copied from mx-sdk-js-core's own src/testdata/adder.abi.json, the exact file
its cookbook uses as its running example.
Call a contract endpoint with native JS args
and Query a read-only view
call this same live contract.
abi.getEndpoints() / abi.getEndpoint(name) return EndpointDefinition
objects whose parameter arrays are named input / output (singular). The raw
ABI JSON uses the plural inputs/outputs; the parsed object does not carry
that naming through. Each parameter's .type is a Type object, and
.getName() gives its readable type name.
Pitfalls
AbiRegistry.remapToKnownTypes() increases the specificity of a registry's
field and parameter types on a best-effort basis. Abi.create() /
AbiRegistry.create() already call this internally before returning, so you do
not need to call it yourself for the common case of loading from JSON, only if
you construct a registry through some other path.
Loading an ABI is a local operation, plus (for the URL case) one plain HTTPS GET for a JSON file, nothing MultiversX-specific about that request, and no wallet or devnet EGLD is needed. If you expected a devnet wallet requirement here, you are thinking of Call a contract endpoint with native JS args.
foo and bar are not endpoints on any real deployed contract. This shape
exists only to show what Abi.create() expects when you know a contract's
interface but do not have its ABI JSON file. Do not copy it expecting it to call
anything real.
See also
- Call a contract endpoint with native JS args
uses this same adder ABI to call
add(value)on the real deployed contract. - Query a read-only view
queries the same contract's
getSum(). - Call a payable endpoint with EGLD is the ABI-driven pattern applied to a payable endpoint.