Call Internet Computer Canister Smart Contracts In NodeJS

How to query canister smart contracts on the Internet Computer in a NodeJS context.

Apr 22, 2022

#javascript #nodejs #web3 #tutorial

https://unsplash.com/photos/oMpAz-DN-9I

Photo by Greg Rakozy on Unsplash


I spent last few months developing Papyrs an open-source, privacy-first, decentralized blogging platform that lives 100% on chain. This new web editor is finally ready for testing, I can write some blog posts again 😁.

This new web3 platform uses DFINITY's Internet Computer. Because each registered user gets two smart contracts, it was particularly useful that I develop scripts to administrate these canisters - e.g. querying remaining cycles or updating code.

As a frontend developer, I am more familiar with NodeJS than any other scripting languages. That's why I used this engine to implement my tools.


Getting Started

Calling the default greet(name: Text) query function that is generated by dfx new <PROJECT\_NAME> might be an interesting example.

actor { public func greet(name : Text) : async Text { return "Hello, " # name # "!"; }; };

That's why in following chapters, we will implement a script - let's call it hello.mjs - that queries this particular function in NodeJS.

try { // TODO: implement query function const result = await query(); console.log(`Result of canister call: ${result}`); } catch (err) { console.error(`Error while querying.`, err); }

Note: if you wish to follow this post step by step, you can initialize a new sample project with dfx new helloworld.

Once created, switch directory cd helloworld, start a local simulated network dfx start --background and deploy the project dfx deploy.


ECMAScript modules

There might be some other ways but I only managed to use both NodeJS LTS and @dfinity/agent libraries with .mjs scripts - i.e. not with common .js scripts.

That's why, the candid JavaScript files that are generated by the dfx build command - the did files - actually need to be converted to ECMAScript modules too.

Basically cp helloworld.did.js hellowrold.did.mjs and that is already it.

Someday the auto-generated files might be generated automatically as modules too but I have to admit, I did not even bother to open a feature request about it.

In my project, of course I automated the copy with a NodeJS script as well (🤪). If it can be useful, here's the code snippet:

import {readFileSync, writeFileSync} from 'fs'; const copyJsToMjs = () => { const srcFolder = './src/declarations/helloworld'; const buffer = readFileSync(`${srcFolder}/helloworld.did.js`); writeFileSync(`${srcFolder}/helloworld.did.mjs`, buffer.toString('utf-8')); }; try { copyJsToMjs(); console.log(`IC types copied!`); } catch (err) { console.error(`Error while copying the types.`, err); }

Script "Hello World"

NodeJS v18 introduces the experimental native support of the fetch command. For LTS version, node-fetch is required.

npm i node-fetch -D

No further dependencies than those provided by the template need to be installed.

To query the IC (Internet Computer) with use agent-js. We create an actor for the candid interface and we effectively call the function greet('world').

const query = async () => { const actor = await actorIC(); return actor.greet('world'); }

The initialization of the actor is very similar to the frontend code that is provided by the default template. However there is two notable differences that are needed to query the IC in a NodeJS context:

  • a host has to be provided because the runtime time environment is not a browser and the code is not served by an "asset" canister
  • node-fetch is required to provide agent-js a way to execute network requests
import fetch from "node-fetch"; import pkgAgent from '@dfinity/agent'; const {HttpAgent, Actor} = pkgAgent; import {idlFactory} from './src/declarations/helloworld/helloworld.did.mjs'; export const actorIC = async () => { // TODO: implement actor initialization const canisterId = actorCanisterIdLocal(); const host = 'http://localhost:8000/'; // Mainnet: 'https://ic0.app' const agent = new HttpAgent({fetch, host}); // Local only await agent.fetchRootKey(); return Actor.createActor(idlFactory, { agent, canisterId }); };

Finally the canister ID can be retrieved. Of course we can also hardcode its value but I find it handy to read the information dynamically.

import {readFileSync} from "fs"; import pkgPrincipal from '@dfinity/principal'; const {Principal} = pkgPrincipal; const actorCanisterIdLocal = () => { const buffer = readFileSync('./.dfx/local/canister_ids.json'); const {helloworld} = JSON.parse(buffer.toString('utf-8')); return Principal.fromText(helloworld.local); }; const actorCanisterIdMainnet = () => { const buffer = readFileSync('./canister_ids.json'); const {helloworld} = JSON.parse(buffer.toString('utf-8')); return Principal.fromText(helloworld.ic); };

The script is implemented. Run in a terminal, it outputs the expected result "Hello, world!" 🥳.

capture-d%E2%80%99e%CC%81cran-2022-04-22-a%CC%80-08.37.54.png


Conclusion

Calling canisters in NodeJS is really handy notably to implement tasks that have administrative purpose. In a follow up blog post I will probably share how I enhanced this solution in order to update - install code in my users' canisters. After all, I still need to test Papyrs 😉.

To infinity and beyond
David