Arbitrary ICRC1 Transaction Histories 1 SHOT.
This one is for @dickhery
given a principal and index canister ID fetches and displays the transaction history for any icrc1 token.
Just so you’re aware, this does not work for ICP because ICP uses a different schema… lol.
I have not managed to get both ICP and all ICRC1 tokens working together in one prompt. separately with a few prompt yes. but this is a 1 shot.

1 Prompt:
This is a frontend-only Internet Computer application.
main.mo = "actor {}" (no backend logic).
This is a web 3 application.
The frontend must use agent host: "https://ic0.app"
Visitors can access the Web3 functionality of the app through the "Web 3 Console" that can be toggled on and off from the bottom of the screen. This console should be semi transparent.
This console allows 1 command.
get transactions <principal> <icrc1 index canister>
This command fetches and prints the transaction history for all transactions of the given principal for that index canister.
use the following code reference for implementation details:
import { Principal } from '@dfinity/principal';
import { Actor, HttpAgent } from '@dfinity/agent';
import { IDL } from '@dfinity/candid';
/**
* Shared ICRC-1 Index Canister Interface
*
* This module provides a universal interface for querying transaction history
* from all ICRC-1 index canisters (ckBTC, ckETH, ckUSDT, etc.) using the standardized
* get_account_transactions function based on the verified ckBTC definition.
*/
// Transaction types from ICRC-1 Index canisters
export interface ICRC1Transaction {
id: bigint;
timestamp: bigint; // nanoseconds (nat64)
kind: string; // "mint", "burn", "transfer", or "approve"
from?: string;
to?: string;
amount: bigint; // raw token amount (nat)
fee?: bigint; // raw fee amount (nat)
memo?: bigint;
spender?: string;
}
export interface ICRC1GetTransactionsResult {
balance: bigint; // raw balance (nat)
transactions: ICRC1Transaction[];
oldest_tx_id?: bigint; // nat
}
// ICRC-1 Index canister types
interface ICRC1Account {
owner: Principal;
subaccount: [] | [Uint8Array];
}
interface ICRC1GetAccountTransactionsArgs {
account: ICRC1Account;
start?: [] | [bigint];
max_results: bigint;
}
interface ICRC1Tokens {
e8s?: bigint; // For ICP-style tokens
amount?: bigint; // For generic ICRC-1 tokens
}
interface ICRC1Timestamp {
timestamp_nanos: bigint;
}
interface ICRC1TransferOperation {
to: ICRC1Account;
fee?: [] | [bigint];
from: ICRC1Account;
amount: bigint;
spender?: [] | [ICRC1Account];
memo?: [] | [Uint8Array];
created_at_time?: [] | [bigint];
}
interface ICRC1MintOperation {
to: ICRC1Account;
amount: bigint;
memo?: [] | [Uint8Array];
created_at_time?: [] | [bigint];
}
interface ICRC1BurnOperation {
from: ICRC1Account;
amount: bigint;
spender?: [] | [ICRC1Account];
memo?: [] | [Uint8Array];
created_at_time?: [] | [bigint];
}
interface ICRC1ApproveOperation {
fee?: [] | [bigint];
from: ICRC1Account;
amount: bigint;
expected_allowance?: [] | [bigint];
expires_at?: [] | [bigint];
spender: ICRC1Account;
memo?: [] | [Uint8Array];
created_at_time?: [] | [bigint];
}
interface ICRC1IndexTransaction {
burn?: [] | [ICRC1BurnOperation];
kind: string; // "mint", "burn", "transfer", or "approve"
mint?: [] | [ICRC1MintOperation];
approve?: [] | [ICRC1ApproveOperation];
timestamp: bigint;
transfer?: [] | [ICRC1TransferOperation];
}
interface ICRC1TransactionWithId {
id: bigint; // nat (block index)
transaction: ICRC1IndexTransaction;
}
interface ICRC1GetTransactions {
balance: bigint; // nat
transactions: ICRC1TransactionWithId[];
oldest_tx_id?: [] | [bigint]; // nat
}
interface ICRC1GetTransactionsErr {
message: string;
}
type ICRC1GetTransactionsResult_Response =
| { Ok: ICRC1GetTransactions }
| { Err: ICRC1GetTransactionsErr };
interface ICRC1IndexCanister {
get_account_transactions: (args: ICRC1GetAccountTransactionsArgs) => Promise<ICRC1GetTransactionsResult_Response>;
}
/**
* Create the universal ICRC-1 Index canister IDL factory.
* This factory works with all ICRC-1 compliant index canisters (ckBTC, ckETH, ckUSDT, etc.)
* based on the verified ckBTC index canister definition.
*/
const createICRC1IndexIdlFactory = () => {
return ({ IDL }: any) => {
const SubAccount = IDL.Vec(IDL.Nat8);
const Account = IDL.Record({
owner: IDL.Principal,
subaccount: IDL.Opt(SubAccount),
});
const GetAccountTransactionsArgs = IDL.Record({
max_results: IDL.Nat,
start: IDL.Opt(IDL.Nat),
account: Account,
});
const Approve = IDL.Record({
fee: IDL.Opt(IDL.Nat),
from: Account,
memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
created_at_time: IDL.Opt(IDL.Nat64),
amount: IDL.Nat,
expected_allowance: IDL.Opt(IDL.Nat),
expires_at: IDL.Opt(IDL.Nat64),
spender: Account,
});
const Burn = IDL.Record({
from: Account,
memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
created_at_time: IDL.Opt(IDL.Nat64),
amount: IDL.Nat,
spender: IDL.Opt(Account),
});
const Mint = IDL.Record({
to: Account,
memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
created_at_time: IDL.Opt(IDL.Nat64),
amount: IDL.Nat,
});
const Transfer = IDL.Record({
to: Account,
fee: IDL.Opt(IDL.Nat),
from: Account,
memo: IDL.Opt(IDL.Vec(IDL.Nat8)),
created_at_time: IDL.Opt(IDL.Nat64),
amount: IDL.Nat,
spender: IDL.Opt(Account),
});
const Transaction = IDL.Record({
burn: IDL.Opt(Burn),
kind: IDL.Text,
mint: IDL.Opt(Mint),
approve: IDL.Opt(Approve),
timestamp: IDL.Nat64,
transfer: IDL.Opt(Transfer),
});
const TransactionWithId = IDL.Record({
id: IDL.Nat,
transaction: Transaction,
});
const GetTransactions = IDL.Record({
balance: IDL.Nat,
transactions: IDL.Vec(TransactionWithId),
oldest_tx_id: IDL.Opt(IDL.Nat),
});
const GetTransactionsErr = IDL.Record({
message: IDL.Text,
});
const GetTransactionsResult = IDL.Variant({
Ok: GetTransactions,
Err: GetTransactionsErr,
});
return IDL.Service({
get_account_transactions: IDL.Func(
[GetAccountTransactionsArgs],
[GetTransactionsResult],
['query']
),
});
};
};
/**
* Create an ICRC-1 Index canister actor for querying account transactions.
* Uses the universal IDL factory that works with all ICRC-1 compliant index canisters.
*
* @param indexCanisterId - The canister ID of the ICRC-1 index canister
* @param identity - Optional identity for authenticated requests
*/
export const createICRC1IndexActor = async (
indexCanisterId: string,
identity?: any
): Promise<ICRC1IndexCanister> => {
const host = 'https://ic0.app';
const agent = await HttpAgent.create({
host,
identity,
});
const idlFactory = createICRC1IndexIdlFactory();
return Actor.createActor(idlFactory, {
agent,
canisterId: indexCanisterId,
}) as unknown as ICRC1IndexCanister;
};
/**
* Helper function to convert ICRC-1 Account to principal string
*/
const accountToPrincipalString = (account: ICRC1Account): string => {
return account.owner.toString();
};
/**
* Helper function to extract optional value from Candid optional array
*/
const extractOptional = <T>(opt: [] | [T] | T | undefined): T | undefined => {
if (opt === undefined || opt === null) return undefined;
if (Array.isArray(opt)) {
return opt.length > 0 ? opt[0] : undefined;
}
return opt as T;
};
/**
* Get transaction history for an ICRC-1 token using its index canister.
*
* This function queries any ICRC-1 index canister using the standardized
* get_account_transactions interface. It works with ckBTC, ckETH, ckUSDT, and other
* ICRC-1 compliant tokens.
*
* @param indexCanisterId - The canister ID of the token's index canister
* @param principal - The principal to query transactions for
* @param maxResults - Maximum number of transactions to return (default: 100)
* @returns Transaction history with balance and transactions
*/
export async function getICRC1AccountTransactions(
indexCanisterId: string,
principal: Principal,
maxResults: number = 100
): Promise<ICRC1GetTransactionsResult> {
try {
const indexCanister = await createICRC1IndexActor(indexCanisterId);
const args: ICRC1GetAccountTransactionsArgs = {
account: {
owner: principal,
subaccount: [],
},
start: [],
max_results: BigInt(maxResults),
};
const response = await indexCanister.get_account_transactions(args);
if ('Err' in response) {
throw new Error(response.Err.message);
}
const result = response.Ok;
// Map transactions into the UI format
const transactions: ICRC1Transaction[] = result.transactions.map((txWithId) => {
const tx = txWithId.transaction;
const id = txWithId.id;
// Extract timestamp (always present as nat64)
const timestamp = tx.timestamp;
// Extract memo (optional)
let memo = BigInt(0);
// Determine operation type based on kind field and extract relevant data
const kind = tx.kind.toLowerCase();
if (kind === 'transfer') {
const transfer = extractOptional(tx.transfer);
if (!transfer) {
return {
id,
timestamp,
kind: 'transfer',
amount: BigInt(0),
memo,
};
}
const from = accountToPrincipalString(transfer.from);
const to = accountToPrincipalString(transfer.to);
const amount = transfer.amount;
const fee = extractOptional(transfer.fee);
return {
id,
timestamp,
kind: 'transfer',
from,
to,
amount,
fee,
memo,
};
} else if (kind === 'mint') {
const mint = extractOptional(tx.mint);
if (!mint) {
return {
id,
timestamp,
kind: 'mint',
amount: BigInt(0),
memo,
};
}
const to = accountToPrincipalString(mint.to);
const amount = mint.amount;
return {
id,
timestamp,
kind: 'mint',
to,
amount,
memo,
};
} else if (kind === 'burn') {
const burn = extractOptional(tx.burn);
if (!burn) {
return {
id,
timestamp,
kind: 'burn',
amount: BigInt(0),
memo,
};
}
const from = accountToPrincipalString(burn.from);
const amount = burn.amount;
return {
id,
timestamp,
kind: 'burn',
from,
amount,
memo,
};
} else if (kind === 'approve') {
const approve = extractOptional(tx.approve);
if (!approve) {
return {
id,
timestamp,
kind: 'approve',
amount: BigInt(0),
memo,
};
}
const from = accountToPrincipalString(approve.from);
const spender = accountToPrincipalString(approve.spender);
const amount = approve.amount;
const fee = extractOptional(approve.fee);
return {
id,
timestamp,
kind: 'approve',
from,
spender,
amount,
fee,
memo,
};
}
// Fallback for unknown transaction types
return {
id,
timestamp,
kind: kind || 'unknown',
amount: BigInt(0),
memo,
};
});
// Handle optional oldest_tx_id
const oldestTxId = extractOptional(result.oldest_tx_id);
return {
balance: result.balance,
transactions,
oldest_tx_id: oldestTxId,
};
} catch (error) {
console.error(`Error loading transactions from index canister ${indexCanisterId}:`, error);
throw new Error('Error loading transactions');
}
}
/**
* Parse transaction kind to determine if it's a send or receive
*
* @param transaction - The transaction to parse
* @param principalString - The principal string to check against
* @returns 'send' | 'receive' | 'other'
*/
export function getICRC1TransactionType(
transaction: ICRC1Transaction,
principalString: string
): 'send' | 'receive' | 'other' {
const kind = transaction.kind.toLowerCase();
if (kind === 'transfer') {
if (transaction.from === principalString) {
return 'send';
} else if (transaction.to === principalString) {
return 'receive';
}
} else if (kind === 'mint') {
if (transaction.to === principalString) {
return 'receive';
}
} else if (kind === 'burn') {
if (transaction.from === principalString) {
return 'send';
}
}
return 'other';
}
/**
* Safely convert BigInt timestamp (nanoseconds) to number (milliseconds) for Date operations.
* Handles potential overflow by clamping to valid Date range.
*
* @param nanos - Timestamp in nanoseconds as BigInt
* @returns Timestamp in milliseconds as number
*/
export function bigIntNanosToMillis(nanos: bigint): number {
try {
const millis = nanos / 1_000_000n;
const MAX_SAFE_MILLIS = 8_640_000_000_000_000n;
if (millis > MAX_SAFE_MILLIS) {
return Number(MAX_SAFE_MILLIS);
}
if (millis < -MAX_SAFE_MILLIS) {
return Number(-MAX_SAFE_MILLIS);
}
return Number(millis);
} catch (error) {
console.error('Error converting BigInt timestamp:', error);
return Date.now();
}
}
/**
* Format BigInt timestamp (nanoseconds) to human-readable string.
*
* @param nanos - Timestamp in nanoseconds as BigInt
* @returns Formatted date string
*/
export function formatBigIntTimestamp(nanos: bigint): string {
const millis = bigIntNanosToMillis(nanos);
const date = new Date(millis);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
before you begin what questions do you have?