Skip to main content

Node.js

Intermediate
Agents
Tutorial

Overview

Node.js is a runtime environment for JavaScript. To interact with a canister through Node.js, you can use the JavaScript agent. Using Node.js can enable use cases such as running an oracle, connecting an existing Node.js application to ICP, or introducing a websocket layer to your application.

More information about calling ICP from JavaScript in a web browser.

Identity management

Before you set up an actor, you will need to have an identity. You can set up an identity that will resolve a seed phrase to a principal by using the following code:

// identity.js
import { Secp256k1KeyIdentity } from "@dfinity/identity-secp256k1";

// Completely insecure seed phrase. Do not use for any purpose other than testing.
// Resolves to "rwbxt-jvr66-qvpbz-2kbh3-u226q-w6djk-b45cp-66ewo-tpvng-thbkh-wae"
const seed = "test test test test test test test test test test test test";

export const identity = await Secp256k1KeyIdentity.fromSeedPhrase(seed);

The seed phrase in this example is derived from the word test repeated 12 times for testing purposes and local development. When you deploy your contract to ICP, you should change the seed to something private.

Remember to store any seed phrase you use in production in a secure place. Use environment variables, and never commit a real seed phrase in plain text to your codebase.

Asset uploading

A community library, @dfinity/assets, is available to upload assets to your project's asset canister. You'll need to get the canister ID, pass your agent, and then upload your files. Below is an example code snippet showcasing this workflow:

import { Blob } from "buffer";
global.Blob = Blob;
import { AssetManager } from "@dfinity/assets";
// ...

const assetCanisterId = localCanisterIds.asset.local;
const assetManager = new AssetManager({
  canisterId: assetCanisterId,
  agent, // re-use agent
  concurrency: 32, // Optional (default: 32), max concurrent requests.
  maxSingleFileSize: 450000, // Optional bytes (default: 450000), larger files will be uploaded as chunks.
  maxChunkSize: 1900000, // Optional bytes (default: 1900000), size of chunks when file is uploaded as chunks.
});

async function main() {
  // ...
  const uploadedFilePath = `token/${idx}${path.extname(node_example.asset)}`;
  const uploadedThumbnailPath = `thumbnail/${idx}.jpeg`;

  await assetManager.store(file, { fileName: uploadedFilePath });
  await assetManager.store(thumbnail, { fileName: uploadedThumbnailPath });
}

This example is a code snippet that is part of a larger code file. This snippet may return an error if run on its own. To view the full code file for context, please see NFT example.

WebSockets

ICP doesn't natively support WebSockets, though the external package ic-websocket-js can be used to implement WebSockets for ICP dapps.

You can install this package with the command:

npm install --save ic-websocket-sdk

If your project uses any @dfinity/… package, it is recommended to use version v0.20.1 or newer.

Then, define the constructor of the IcWebSocket class:

import { canisterId } from "../../declarations/node_example";
import IcWebSocket, { generateRandomIdentity, createWsConfig } from "ic-websocket-js";
import { node_example } from "../../declarations/node_example";

const gatewayUrl = "ws://127.0.0.1:8080";
const icUrl = "http://127.0.0.1:4943";

const wsConfig = createWsConfig({
  canisterId: backendCanisterId,
  canisterActor: node_example,
  identity: generateRandomIdentity(),
  networkUrl: icUrl,
});

const ws = new IcWebSocket(gatewayUrl, undefined, wsConfig);

This implementation is similar to the native browser WebSocket API, though there are some parameters that need to be configured:

  • canisterId: Your project's backend canister ID.

  • canisterActor: Imported from your canister's generated declarations (dfx generate) and used to serialize and deserialize the WebSocket messages automatically.

  • gatewayUrl: The WebSocket URL of the gateway.

  • networkUrl: The URL of the local replica.

  • identity: The identity that the SDK uses to sign the client’s messages. Here you can pass the user’s Internet Identity or use a generateRandomIdentity helper function to generate a random temporary one.

Now you can specify your business logic and declare the callback for each WebSocket event:

import { canisterId } from "../../declarations/node_example";
import IcWebSocket, { generateRandomIdentity, createWsConfig } from "ic-websocket-js";
import { node_example } from "../../declarations/node_example";

const gatewayUrl = "ws://127.0.0.1:8080";
const icUrl = "http://127.0.0.1:4943";

const wsConfig = createWsConfig({
  canisterId: backendCanisterId,
  canisterActor: node_example,
  identity: generateRandomIdentity(),
  networkUrl: icUrl,
});

const ws = new IcWebSocket(gatewayUrl, undefined, wsConfig);

ws.onopen = () => {
  console.log("Connected to the canister");
};

ws.onmessage = async (event) => {
  console.log("Received message:", event.data);

  const messageToSend = {
    text: event.data.text + "-pong",
  };
  ws.send(messageToSend);
};

ws.onclose = () => {
  console.log("Disconnected from the canister");
};

ws.onerror = (error) => {
  console.log("Error:", error);
};

You can view a full example using this library with a Rust backend or a Motoko backend.

Learn more about WebSockets on ICP.

Testing

To create an end2end (e2e) test for your project's backend canister using agent-js, first create a new test file in your project's src/tests/ subdirectory. Name the file e2e_tests_backend.test.ts, then insert the following content:

import { expect, test } from "vitest";
import { Actor, CanisterStatus, HttpAgent } from "@dfinity/agent";
import { Principal } from "@dfinity/principal";
import { e2e_tests_backendCanister, e2e_tests_backend } from "./actor";

test("should handle a basic greeting", async () => {
  const result1 = await e2e_tests_backend.greet("test");
  expect(result1).toBe("Hello, test!");
});

This test will do the following:

  • Imports dependent packages and the agent's functions from the actor.js file.

  • Defines a test method that accepts two arguments. Inside this method, expect is used to check the result of the backend canister's greet function against the expected result.

This test is written for the default backend canister.

To run this test, you will need to deploy your project and generate the necessary declarations for your canister. Learn more about creating and deploying a project.

Once you have deployed your canisters and generated the declarations, run your test with the command:

npm test

The test should be successful and return output such as:

 ✓ src/tests/e2e_tests_backend.test.ts (1)
   ✓ should handle a basic greeting

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  16:24:03
   Duration  205ms (transform 32ms, setup 0ms, collect 68ms, tests 11ms, environment 0ms, prepare 41ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

Learn more about testing with an agent and view a more complex test example.

NFT example

To demonstrate how you can use Node.js to implement functionality to upload assets, create an NFT collection, and mint an NFT for an identity, the following agent-js src/node/index.js script can be used in conjunction with the DIP-721 NFT backend canister example:

import fetch from "isomorphic-fetch";
import { HttpAgent } from "@dfinity/agent";
import { canisterId, createActor } from "../declarations/nft/index.js";
import { identity } from "./identity.js";
import { createRequire } from "node:module";
import path from "path";
import fs from "fs";
import mmm from "mmmagic";
import { fileURLToPath } from "url";
import sha256File from "sha256-file";
import { Blob } from "buffer";
import { AssetManager } from "@dfinity/assets";
import imageThumbnail from "image-thumbnail";
import prettier from "prettier";

const require = createRequire(import.meta.url);
const nftConfig = require("./nfts.json");
const localCanisterIds = require("../../.dfx/local/canister_ids.json");

const encoder = new TextEncoder();

const agent = new HttpAgent({
  identity: await identity,
  host: "http://127.0.0.1:4943",
  fetch,
});
const effectiveCanisterId =
  canisterId?.toString() ?? localCanisterIds.nft.local;
const assetCanisterId = localCanisterIds.assets.local;
const actor = createActor(effectiveCanisterId, {
  agent,
});
const assetManager = new AssetManager({
  canisterId: assetCanisterId,
  agent,
  concurrency: 32, // Optional (default: 32), max concurrent requests.
  maxSingleFileSize: 450000, // Optional bytes (default: 450000), larger files will be uploaded as chunks.
  maxChunkSize: 1900000, // Optional bytes (default: 1900000), size of chunks when file is uploaded as chunks.
});

async function main() {
  nftConfig.forEach(async (nft, idx) => {
    console.log("starting upload for " + nft.asset);

    // Parse metadata, if present
    const metadata = Object.entries(nft.metadata ?? []).map(([key, value]) => {
      return [key, { TextContent: value }];
    });

    const filePath = path.join(
      fileURLToPath(import.meta.url),
      "..",
      "assets",
      nft.asset
    );

    const file = fs.readFileSync(filePath);

    const sha = sha256File(filePath);
    const options = {
      width: 256,
      height: 256,
      responseType: "buffer",
      jpegOptions: { force: true, quality: 90 },
    };
    console.log("generating thumbnail");
    const thumbnail = await imageThumbnail(filePath, options);

    const magic = await new mmm.Magic(mmm.MAGIC_MIME_TYPE);
    const contentType = await new Promise((resolve, reject) => {
      magic.detectFile(filePath, (err, result) => {
        if (err) reject(err);
        resolve(result);
      });
    });
    console.log("detected contenttype of ", contentType);

    /**
     * For asset in nfts.json
     *
     * Take asset
     * Check extension / mimetype
     * Sha content
     * Generate thumbnail
     * Upload both to asset canister -> file paths
     * Generate metadata from key / value
     * Mint ^
     */

    const uploadedFilePath = `token/${idx}${path.extname(nft.asset)}`;
    const uploadedThumbnailPath = `thumbnail/${idx}.jpeg`;

    console.log("uploading asset to ", uploadedFilePath);
    await assetManager.store(file, { fileName: uploadedFilePath });
    console.log("uploading thumbnail to ", uploadedThumbnailPath);
    await assetManager.store(thumbnail, { fileName: uploadedThumbnailPath });

    const principal = await (await identity).getPrincipal();

    const data = [
      [
        "location",
        {
          TextContent: `http://${assetCanisterId}.localhost:4943/${uploadedFilePath}`,
        },
      ],
      [
        "thumbnail",
        {
          TextContent: `http://${assetCanisterId}.localhost:4943/${uploadedThumbnailPath}`,
        },
      ],
      ["contentType", { TextContent: contentType }],
      ["contentHash", { BlobContent: encoder.encode(sha) }],
      ...metadata,
    ];
    const mintResult = await actor.dip721_mint(principal, BigInt(idx), data);
    console.log("result: ", mintResult);
    const metaResult = await actor.tokenMetadata(0n);
    console.log("new token info: ", metaResult);
    console.log(
      "token metadata: ",
      prettier.format(
        JSON.stringify(metaResult, (key, value) =>
          typeof value === "bigint" ? value.toString() : value
        ),
        { parser: "json" }
      )
    );
  });
}
main();

Resources