Skip to content

Workspace and shared helpers (code examples) ​

The pnpm workspace every example runs in, and @trne/utils, the helpers the scripts import.

Copied from Futureverse’s futureversecom/trn-examples, deleted with its GitHub organisation, at commit 44c9e16 of 2024-10-31. Apache-2.0: license.

Offline destinations

Some places this page points to did not answer when last checked:

  • ghcr.io/futureversecom — organisation deleted: its container images can no longer be pulled (checked 2026-10-11), named in code

Keep any copy of the node image a machine already has, since it cannot be pulled again; the node source survives as the mirror https://github.com/marco-fv/trn-seed-public.

Install once, at the workspace root, with pnpm install. An example’s scripts run with tsx, and tsconfig.json maps @trne/utils/* to packages/utils/src/*. docker-compose.yml started a local node for the scripts’ "local" network.

Workspace files ​

package.json ​

json
{
  "name": "@therootnetwork/examples",
  "version": "1.0.0",
  "description": "A curated collection of examples and solutions on how to interact with the Root Network",
  "main": "index.js",
  "repository": "git@github.com:futureversecom/trn-examples.git",
  "license": "MIT",
  "scripts": {
    "lint": "prettier --check . && eslint \"**/*.{ts,tsx}\"",
    "format": "prettier --config .prettierrc.js --write .",
    "type-check": "tsc --noEmit --jsx react-jsx"
  },
  "dependencies": {
    "envalid": "^7.3.1",
    "ethers": "^5.7.2",
    "@polkadot/api": "^10.9.1",
    "@polkadot/keyring": "^12.3.2",
    "@polkadot/util": "^12.3.2",
    "@polkadot/util-crypto": "^12.3.2",
    "@therootnetwork/api": "^1.0.2",
    "@therootnetwork/evm": "^1.0.3"
  },
  "devDependencies": {
    "@eslint/create-config": "^0.4.3",
    "@polkadot/types": "^10.9.1",
    "@therootnetwork/api-types": "^1.0.0",
    "@trivago/prettier-plugin-sort-imports": "^4.1.1",
    "@types/node": "^20.2.1",
    "@typescript-eslint/eslint-plugin": "^5.59.6",
    "@typescript-eslint/parser": "^5.59.6",
    "eslint": "^8.40.0",
    "prettier": "^2.8.8",
    "tsconfig-paths": "^4.2.0",
    "tsx": "^3.12.10",
    "typescript": "^5.0.4"
  }
}

pnpm-workspace.yaml ​

yaml
packages:
  - 'examples/**'
  - 'packages/**'

tsconfig.json ​

json
{
  "compilerOptions": {
    "target": "es2021",
    "module": "commonjs",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "paths": {
      "@trne/utils/*": ["./packages/utils/src/*"],
      "@trne/ui-utils/*": ["./packages/ui/*"]
    }
  }
}

.npmrc ​

ini
auto-install-peers=true
ignore-workspace-root-check=true

docker-compose.yml ​

yaml
version: "3"
services:
  node:
    image: ghcr.io/futureversecom/seed:latest
    volumes:
      - ./.docker/node:/node-data
    command:
      - --dev
      - --alice
      - --base-path=/node-data
      - --unsafe-rpc-external
      - --unsafe-ws-external
      - --node-key=6c2276fece04635327774eea8584a958e69652f45572e1e4cd2211bfb7221943
      - --rpc-cors=all
    ports:
      - "9933:9933"
      - "9944:9944"
      - "30333:30333"

@trne/utils ​

packages/utils/package.json ​

json
{
  "name": "@trne/utils",
  "dependencies": {
    "realm-web": "^2.0.0",
    "pino": "^8.15.1",
    "pino-pretty": "^10.2.0",
    "pretty-error": "^4.0.0"
  }
}

packages/utils/src/accounts.ts ​

ts
export const ALICE = "0xE04CC55ebEE1cBCE552f250e85c57B70B2E2625b";
export const BOB = "0x25451A4de12dcCc2D166922fA938E900fCc4ED24";
export const CHARLIE = "0x5630a480727CD7799073b36472d9b1A6031F840b";

packages/utils/src/constants.ts ​

ts
export const RootNetwork = {
	root: {
		MongoAppId: "mainnet-web-apps-fdmlm",
	},

	porcini: {
		MongoAppId: "porcini-web-apps-hvfnd",
	},
};

packages/utils/src/createKeyring.ts ​

ts
import { Keyring } from "@polkadot/keyring";
import { hexToU8a } from "@polkadot/util";

export type Signer = ReturnType<InstanceType<typeof Keyring>["addFromSeed"]>;

export function createKeyring(seed: string): Signer {
	const keyring = new Keyring({ type: "ethereum" });
	const seedU8a = hexToU8a(seed);
	return keyring.addFromSeed(seedU8a);
}

packages/utils/src/fetchFinalisedHead.ts ​

ts
import type { ApiPromise } from "@polkadot/api";

/**
 * Fetch the current finalised block height
 * @param api Instance of ApiPromise
 * @returns Block height
 */
export async function fetchFinalisedHead(api: ApiPromise): Promise<number> {
	const blockHash = await api.rpc.chain.getFinalizedHead();
	const { block } = await api.rpc.chain.getBlock(blockHash);
	return block.header.number.toNumber();
}

packages/utils/src/filterExtrinsicEvents.ts ​

ts
import { SubmittableResultValue } from "@polkadot/api/types";
import { EventRecord } from "@polkadot/types/interfaces";
import assert from "node:assert";

import { type EventData, formatEventData } from "./formatEventData";

type EventFilterByName = `${string}.${string}`;
type EventFilterByData = {
	name: `${string}.${string}`;
	key: string;
	data: EventData["x"];
};

type EventFilter = EventFilterByName | EventFilterByData;

/**
 * Filters extrinsic events based on provided event filters.
 *
 * This function filters a list of extrinsic events based on the provided event filters,
 * returning an array of EventRecord objects matching the filter criteria.
 *
 * @param events - An array of extrinsic events to filter.
 * @param eventFilters - An array of EventFilter objects specifying criteria for filtering events.
 * @returns An array of EventRecord objects that match the filter criteria.
 * @throws An assertion error if the input events array is falsy.
 */
export function filterExtrinsicEvents(
	events: SubmittableResultValue["events"],
	eventFilters: EventFilter[]
): EventRecord[] {
	assert(events);

	return eventFilters.map((eventFilter) => {
		const event = events.find(({ event }) => {
			const name = `${event.section[0].toUpperCase() + event.section.slice(1)}.${
				event.method
			}` as `${string}.${string}`;

			if (typeof eventFilter === "string") return name === eventFilter;

			if (name !== eventFilter.name) return;
			const eventData = formatEventData(event);
			return JSON.stringify(eventData[eventFilter.key]) === JSON.stringify(eventFilter.data);
		});

		assert(event);
		return event;
	});
}

packages/utils/src/filterTransactionEvents.ts ​

ts
import { ContractReceipt, utils } from "ethers";
import assert from "node:assert";

export function filterTransactionEvents(
	abi: string[],
	logs: ContractReceipt["logs"],
	eventNames: string[]
) {
	const int = new utils.Interface(abi);

	return eventNames.map((eventName) => {
		const event = logs.map((log) => int.parseLog(log)).find((event) => event.name === eventName);
		assert(event);
		return event;
	});
}

packages/utils/src/formatEventData.ts ​

ts
import { EventRecord } from "@polkadot/types/interfaces";

export type EventData = Record<string, { type: string; value: unknown }>;

/**
 * Formats raw event data into a structured EventData object.
 *
 * @param event - The raw event data to format.
 * @returns An EventData object containing the formatted event data.
 */
export function formatEventData(event: EventRecord["event"]): EventData {
	const data = event.data.toJSON() as unknown[];
	const fields = event.meta.fields.toJSON() as { name: string; typeName: string }[];

	return fields.reduce((record, { name, typeName: type }, index) => {
		record[name ?? `arg${index}`] = { value: data[index], type };
		return record;
	}, {} as EventData);
}

packages/utils/src/getChainApi.ts ​

ts
import { ApiPromise } from "@polkadot/api";
import {
	getApiOptions,
	getLocalProvider,
	getPublicProvider,
	NetworkName,
} from "@therootnetwork/api";
import "@therootnetwork/api-types";

async function getChainApi(name: NetworkName | "local") {
	const api = await ApiPromise.create({
		noInitWarn: true,
		...getApiOptions(),
		...(name === "local" ? getLocalProvider() : getPublicProvider(name)),
	});

	return api;
}

export { ApiPromise, getChainApi };

packages/utils/src/getERC1155Contract.ts ​

ts
import { collectionIdToERC1155Address, ERC1155_PRECOMPILE_ABI } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getERC1155Contract(collectionId: number) {
	const address = collectionIdToERC1155Address(collectionId);
	return new Contract(address, ERC1155_PRECOMPILE_ABI);
}

packages/utils/src/getERC20Contract.ts ​

ts
import { assetIdToERC20Address, ERC20_ABI } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getERC20Contract(assetId: number): Contract {
	const assetAddress = assetIdToERC20Address(assetId);

	return new Contract(assetAddress, ERC20_ABI);
}

packages/utils/src/getERC721Contract.ts ​

ts
import { collectionIdToERC721Address, ERC721_PRECOMPILE_ABI } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getERC721Contract(collectionId: number) {
	const address = collectionIdToERC721Address(collectionId);
	return new Contract(address, ERC721_PRECOMPILE_ABI);
}

packages/utils/src/getFeeProxyContract.ts ​

ts
import { FEE_PROXY_PRECOMPILE_ABI, FEE_PROXY_PRECOMPILE_ADDRESS } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getFeeProxyContract(): Contract {
	return new Contract(FEE_PROXY_PRECOMPILE_ADDRESS, FEE_PROXY_PRECOMPILE_ABI);
}

packages/utils/src/getFeeProxyPricePair.ts ​

ts
import { BigNumber } from "ethers";
import assert from "node:assert";

import { XRP_ASSET_ID } from "./porcini-assets";
import { EthersProvider } from "./withEthersProvider";

interface AmountsIn {
	Ok: [number, number];
}

export async function getFeeProxyPricePair(
	provider: EthersProvider,
	gasEstimate: BigNumber,
	feeAssetId: number,
	slippage = 0
): Promise<{ maxPayment: BigNumber; maxFeePerGas: BigNumber; estimateGasCost: BigNumber }> {
	const { lastBaseFeePerGas: maxFeePerGas } = await provider.getFeeData();

	assert(maxFeePerGas);

	// convert gasPrice in ETH to gasPrice in XRP, which has different decimals, one is 18 & one is 6
	const gasCostInEth = gasEstimate.mul((maxFeePerGas.toNumber() * (1 + slippage)).toFixed());
	const remainder = gasCostInEth.mod(10 ** 12);
	const gasCostInXRP = gasCostInEth.div(10 ** 12).add(remainder.gt(0) ? 1 : 0);

	// query the the `dex` to determine the `maxPayment`
	const {
		Ok: [maxPayment],
	} = (await provider.send("dex_getAmountsIn", [
		gasCostInXRP.toNumber(),
		[feeAssetId, XRP_ASSET_ID],
	])) as unknown as AmountsIn;

	return {
		estimateGasCost: gasCostInXRP,
		maxPayment: BigNumber.from(maxPayment.toString()),
		maxFeePerGas,
	};
}

packages/utils/src/getFuturepassContract.ts ​

ts
import { FUTUREPASS_PRECOMPILE_ABI } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getFuturepassContract(fpAccount: string): Contract {
	return new Contract(fpAccount, [
		...FUTUREPASS_PRECOMPILE_ABI,
		"function registerDelegateWithSignature(address delegate, uint8 proxyType, uint32 deadline, bytes signature) external",
	]);
}

packages/utils/src/getFuturepassRegistrarContract.ts ​

ts
import {
	FUTUREPASS_REGISTRAR_PRECOMPILE_ABI,
	FUTUREPASS_REGISTRAR_PRECOMPILE_ADDRESS,
} from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getFuturepassRegistrarContract(): Contract {
	return new Contract(FUTUREPASS_REGISTRAR_PRECOMPILE_ADDRESS, FUTUREPASS_REGISTRAR_PRECOMPILE_ABI);
}

packages/utils/src/getLogger.ts ​

ts
import pino from "pino";

export type Logger = ReturnType<typeof getLogger>;

export function getLogger() {
	return pino({
		transport: {
			target: "pino-pretty",
		},
	});
}

packages/utils/src/getNFTContract.ts ​

ts
import { NFT_PRECOMPILE_ABI, NFT_PRECOMPILE_ADDRESS } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getNFTContract(): Contract {
	return new Contract(NFT_PRECOMPILE_ADDRESS, NFT_PRECOMPILE_ABI);
}

packages/utils/src/getSFTContract.ts ​

ts
import { SFT_PRECOMPILE_ABI, SFT_PRECOMPILE_ADDRESS } from "@therootnetwork/evm";
import { Contract } from "ethers";

export function getSFTContract(): Contract {
	return new Contract(SFT_PRECOMPILE_ADDRESS, SFT_PRECOMPILE_ABI);
}

packages/utils/src/porcini-assets.ts ​

ts
// asset ids on Porcini testnet
export const SYLO_ASSET_ID = 3_172;
export const ASTO_ASSET_ID = 17_508;
export const XRP_ASSET_ID = 2;

packages/utils/src/sendExtrinsic.ts ​

ts
import {
	AddressOrPair,
	SignerOptions,
	SubmittableExtrinsic,
	SubmittableResultValue,
} from "@polkadot/api/types";
import { ISubmittableResult } from "@polkadot/types/types";

export interface SubmittableResponse {
	blockHash: string;
	extrinsicHash: string;
	extrinsicIndex: number;
	extrinsicId: string;
	result: SubmittableResultValue;
}

type SubmitOptions = Partial<SignerOptions> & {
	log?: { info: (msg: string) => void };
};

/**
 * A simple wrapper for `extrisinc.signAndSend` that resolve when the
 * extrinsic is InBlock
 *
 * @param extrinsic - A submitable extrinsic form ApiProimse type
 * @param signer - A keyring pair to sign the extrinsic
 * @param options - Options for Siger when signing the extrisinc
 * @returns - A simple object of SubmittableResponse
 */
export async function sendExtrinsic(
	extrinsic: SubmittableExtrinsic<"promise", ISubmittableResult>,
	signer: AddressOrPair,
	options: SubmitOptions = {}
): Promise<SubmittableResponse> {
	return new Promise((resolve, reject) => {
		let unsubscribe: () => void;
		const { log, ...signerOptions } = options;
		extrinsic
			.signAndSend(signer, signerOptions, (result) => {
				const { status, dispatchError, txHash, txIndex, blockNumber } =
					result as SubmittableResultValue;
				log?.info(`extrinsic status="${status.type}"`);
				if (!status.isFinalized) return;
				if (!txIndex || !blockNumber) return;

				if (!dispatchError) {
					unsubscribe?.();
					const blockHash = status.asFinalized.toString();
					const height = blockNumber.toString().padStart(10, "0");
					const index = txIndex.toString().padStart(6, "0");
					const hash = blockHash.slice(2, 7);
					const extrinsicId = `${height}-${index}-${hash}`;

					return resolve({
						blockHash,
						extrinsicHash: txHash.toString(),
						extrinsicIndex: txIndex,
						extrinsicId,
						result,
					});
				}

				if (!dispatchError.isModule) {
					unsubscribe?.();
					return reject(new Error(`Extrinsic failed ${JSON.stringify(dispatchError.toJSON())}`));
				}

				const { section, name, docs } = dispatchError.registry.findMetaError(
					dispatchError.asModule
				);
				unsubscribe?.();
				reject(new Error(`Extrinsic sending failed, [${section}.${name}] ${docs}`));
			})
			.then((unsub) => (unsubscribe = unsub))
			.catch((error) => reject(error));
	});
}

packages/utils/src/withChainApi.ts ​

ts
import type { ApiPromise } from "@polkadot/api";
import type { NetworkName } from "@therootnetwork/api";
import { cleanEnv, str } from "envalid";
import PrettyError from "pretty-error";

import { createKeyring, type Signer } from "./createKeyring";
import { getChainApi } from "./getChainApi";
import { getLogger, type Logger } from "./getLogger";

const env = cleanEnv(process.env, {
	CALLER_PRIVATE_KEY: str(), // private key of extrinsic caller
});

const pe = new PrettyError();

export async function withChainApi(
	network: NetworkName | "local",
	callback: (api: ApiPromise, signer: Signer, logger: Logger) => Promise<void>
) {
	const logger = getLogger();

	const [api, signer] = await Promise.all([
		getChainApi(network),
		createKeyring(env.CALLER_PRIVATE_KEY),
	]);

	logger.info(`create an ApiPromise instance with network="${network}"`);
	logger.info(`create a Signer instance from a private key of address="${signer.address}"`);

	await callback(api, signer, logger).catch((error) => console.log(pe.render(error)));

	await api.disconnect();

	logger.info("call ended 🎉 ");
}

packages/utils/src/withEthersProvider.ts ​

ts
import type { NetworkName } from "@therootnetwork/api";
import { getPublicProviderUrl } from "@therootnetwork/evm";
import { cleanEnv, str } from "envalid";
import { providers, Wallet } from "ethers";
import PrettyError from "pretty-error";

import { getLogger, type Logger } from "./getLogger";

const env = cleanEnv(process.env, {
	CALLER_PRIVATE_KEY: str(), // private key of extrinsic caller
});

const pe = new PrettyError();

export type EthersProvider = providers.JsonRpcProvider;

export async function withEthersProvider(
	network: NetworkName | "local",
	callback: (provider: EthersProvider, wallet: Wallet, logger: Logger) => Promise<void>
) {
	const { provider, wallet, logger } = await provideEthersProvider(network);

	await callback(provider, wallet, logger).catch((error) => {
		console.log(pe.render(error));
	});

	logger.info("call ended 🎉 ");
}

export async function provideEthersProvider(network: NetworkName | "local") {
	const logger = getLogger();

	const provider = getEthersProvider(network);

	const wallet = new Wallet(env.CALLER_PRIVATE_KEY, provider);
	logger.info(`create a Wallet instance from a private key of address="${wallet.address}"`);

	return { provider, wallet, logger };
}

export function getEthersProvider(network: NetworkName | "local") {
	const logger = getLogger();

	const provider = new providers.JsonRpcProvider(
		network !== "local" ? getPublicProviderUrl(network) : "http://localhost:9933"
	);
	logger.info(`create a JsonRpcProvider instance with network="${network}"`);
	return provider;
}
Source: futureversecom/trn-examples (deleted) · commit 44c9e16, 2024-10-31 · Apache-2.0, license · part of Code examples

Curated independently. Source-attributed reference for The Root Network. Not affiliated with Futureverse / TRN Labs.