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 provideagent-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!" 🥳.
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