Skip to main content

Node.js

Intermediate
Agents

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.

Identity management

Before you set up an actor, you will need to have an identity.

Using keys through the ICP JavaScript agent

You can configure an authentication identity for Node.js through the ICP JavaScript agent using the ECDSA or Ed25519 private keys.

Seed phrase

Alternatively, 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 project 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.

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 backend 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);
};

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 end-to-end (e2e) test for your project's backend canister using the ICP JavaScript agent, 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 ICP JavaScript agent src/node/index.js script can be used in conjunction with the DIP-721 NFT backend canister example:

import Nat "mo:base/Nat";
import Nat8 "mo:base/Nat8";
import Nat16 "mo:base/Nat16";
import Nat32 "mo:base/Nat32";
import Nat64 "mo:base/Nat64";
import List "mo:base/List";
import Array "mo:base/Array";
import Option "mo:base/Option";
import Bool "mo:base/Bool";
import Principal "mo:base/Principal";
import Types "./Types";

shared actor class Dip721NFT(custodian: Principal, init : Types.Dip721NonFungibleToken) = Self {
stable var transactionId: Types.TransactionId = 0;
stable var nfts = List.nil<Types.Nft>();
stable var custodians = List.make<Principal>(custodian);
stable var logo : Types.LogoResult = init.logo;
stable var name : Text = init.name;
stable var symbol : Text = init.symbol;
stable var maxLimit : Nat16 = init.maxLimit;

// https://forum.dfinity.org/t/is-there-any-address-0-equivalent-at-dfinity-motoko/5445/3
let null_address : Principal = Principal.fromText("aaaaa-aa");

public query func balanceOfDip721(user: Principal) : async Nat64 {
return Nat64.fromNat(
List.size(
List.filter(nfts, func(token: Types.Nft) : Bool { token.owner == user })
)
);
};

public query func ownerOfDip721(token_id: Types.TokenId) : async Types.OwnerResult {
let item = List.find(nfts, func(token: Types.Nft) : Bool { token.id == token_id });
switch (item) {
case (null) {
return #Err(#InvalidTokenId);
};
case (?token) {
return #Ok(token.owner);
};
};
};

public shared({ caller }) func safeTransferFromDip721(from: Principal, to: Principal, token_id: Types.TokenId) : async Types.TxReceipt {
if (to == null_address) {
return #Err(#ZeroAddress);
} else {
return transferFrom(from, to, token_id, caller);
};
};

public shared({ caller }) func transferFromDip721(from: Principal, to: Principal, token_id: Types.TokenId) : async Types.TxReceipt {
return transferFrom(from, to, token_id, caller);
};

func transferFrom(from: Principal, to: Principal, token_id: Types.TokenId, caller: Principal) : Types.TxReceipt {
let item = List.find(nfts, func(token: Types.Nft) : Bool { token.id == token_id });
switch (item) {
case null {
return #Err(#InvalidTokenId);
};
case (?token) {
if (
caller != token.owner and
not List.some(custodians, func (custodian : Principal) : Bool { custodian == caller })
) {
return #Err(#Unauthorized);
} else if (Principal.notEqual(from, token.owner)) {
return #Err(#Other);
} else {
nfts := List.map(nfts, func (item : Types.Nft) : Types.Nft {
if (item.id == token.id) {
let update : Types.Nft = {
owner = to;
id = item.id;
metadata = token.metadata;
};
return update;
} else {
return item;
};
});
transactionId += 1;
return #Ok(transactionId);
};
};
};
};

public query func supportedInterfacesDip721() : async [Types.InterfaceId] {
return [#TransferNotification, #Burn, #Mint];
};

public query func logoDip721() : async Types.LogoResult {
return logo;
};

public query func nameDip721() : async Text {
return name;
};

public query func symbolDip721() : async Text {
return symbol;
};

public query func totalSupplyDip721() : async Nat64 {
return Nat64.fromNat(
List.size(nfts)
);
};

public query func getMetadataDip721(token_id: Types.TokenId) : async Types.MetadataResult {
let item = List.find(nfts, func(token: Types.Nft) : Bool { token.id == token_id });
switch (item) {
case null {
return #Err(#InvalidTokenId);
};
case (?token) {
return #Ok(token.metadata);
}
};
};

public query func getMaxLimitDip721() : async Nat16 {
return maxLimit;
};

public func getMetadataForUserDip721(user: Principal) : async Types.ExtendedMetadataResult {
let item = List.find(nfts, func(token: Types.Nft) : Bool { token.owner == user });
switch (item) {
case null {
return #Err(#Other);
};
case (?token) {
return #Ok({
metadata_desc = token.metadata;
token_id = token.id;
});
}
};
};

public query func getTokenIdsForUserDip721(user: Principal) : async [Types.TokenId] {
let items = List.filter(nfts, func(token: Types.Nft) : Bool { token.owner == user });
let tokenIds = List.map(items, func (item : Types.Nft) : Types.TokenId { item.id });
return List.toArray(tokenIds);
};

public shared({ caller }) func mintDip721(to: Principal, metadata: Types.MetadataDesc) : async Types.MintReceipt {
if (not List.some(custodians, func (custodian : Principal) : Bool { custodian == caller })) {
return #Err(#Unauthorized);
};

let newId = Nat64.fromNat(List.size(nfts));
let nft : Types.Nft = {
owner = to;
id = newId;
metadata = metadata;
};

nfts := List.push(nft, nfts);

transactionId += 1;

return #Ok({
token_id = newId;
id = transactionId;
});
};
}

Resources