Call Internet Computer Canisters In NodeJS

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-js​ 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:​
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!" 🥳.

​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​

​For more adventures, follow me on Twitter