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 commit44c9e16of 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=truedocker-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