Next Project Ideas, help!

Hey guys,

I have some ideas for next projects I want to work on. I want to make sure whatever I work on there is actually someone who might find it useful. Whatever I do, I plan to open source / share, but I want to make sure I’m not completely wasting my time if there is no demand or want for the thing. Here are my ideas, please let me know If you have a preference or there is something you would want to see, that I didn’t think of.

  1. ckBTC wallet in caffeine App that integrates MINTING. - ideally the goal would be a prompt that anyone could get this in one shot. I can already do the ckBTC wallet in one shot, what I havn’t tried yet is the minting from BTC part.

  2. QR code scanner for in person payments (IE user scans a QR code which automatically makes a payment from the wallet app they have open after confirmation).

  3. Transaction History - one shot prompt. I already am able to get transaction histories to work. The hard part is to get caffeine to do it on the first try every time. It has a real hard time with getting the schema correct consistently. Type matching grrr…

  4. Put all these things I’ve been working on together and build a terminal for making queries against the ledger and index canisters. Allowing user to transform the data returned in useful ways. This could be used like a block explorer to do “research.” Maybe it could attempt to identify “swaps” and generatle cls files to help people do their taxes… I don’t know I’m just brainstorming here.

  • 1 shot - ckBTC wallet with minter
  • QR code scanner in person payments
  • 1 shot - transaction histories
  • Power user terminal block explorer thing
  • No one cares
0 voters
3 Likes

Another good answer is “quit wasting your time and learn rust.” But it wont let me add that to the poll :rofl:

A tax return system for the IC would be very, very useful for lots of people. I think it would be a lot of work though. It would also require being able to read from arbitrary IC ledgers, given the need to account for swaps to other tokens.

Number 2 also sounds cool. I voted for both

2 Likes

yah I already have this part figured out… the tricky thing is transforming the data and making sense of it.

1 Like

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.

image

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?
3 Likes

Frontend ckBTC wallet with Minting address 1 SHOT

This one is for @ZackDS

Mandatory disclaimer: As usual, I take zero responsibility for anything this prompt does or fails to do. I do not endorse the management of user funds without an audit of your codebase done by a professional. Use what is presented here at your own risk and due diligence, I take responsibility for absolutely nothing.

when user logs in allows them to access ckBTC wallet.

Integrates with ledger to show correct balance and allows sending ckBTC.

integrates with minter canister to provide correct BTC address to mint ckBTC.

what this prompt does not do: (next steps)

  • transaction history. It was too hard to get this and transaction history working in one prompt. But you could follow up with a similar prompt to above to get it in 2 shots easily.
  • utxo status. does not get status of incoming mint of ckBTC, user just has to wait and wonder.
  • does not support off ramp of ckBTC to BTC, you will be assimilated haha

Please do not actually send BTC to this. Thats why I gave the prompt so anyone can make their own and test it. just gave link for proof of concept. I might delete it tomorrow for all I know.

1 Prompt:

This is a front end only application main.mo = "actor{}"

The frontend must use agent host: "https://ic0.app"


Visitors can access the Web3 functionality of the app through the "ckBTC Console" that can be toggled on and off from the bottom of the screen. This console should be semi transparent.

This console allows 4 commands.

"login" command
initiates internet identity 2.0 authentication via "https://id.ai/authorize"
When the user is logged in their ckBTC balance and principal id (icrc1 address) should be displayed in the upper right hand corner of the app. Clicking on the icrc1

 
The users ckBTC minting address should also be displayed in the upper left corner of the app. There should be an explanation that sending BTC to this address will convert it to ckBTC. to get the ckBTC minting address, the ckBTC minter canister mqygn-kiaaa-aaaar-qaadq-cai must be queried:

get_btc_address: (record {owner:opt principal; subaccount:opt vec nat8}) → (text) 

there must be 2 separate buttons to copy both the icrc1 address and the minting address to the clipboard.

Balance must auto update every 10 seconds. Balance must be displayed with 8 points of decimal precision.

"logout" command
Logs the user out of internet identity.

"send <principal> <value>" command
Sends ckBTC to a valid icrc1 address (principal).

Avoid use of browser incompatible dependencies that require node, i.e. Buffer. 


ckBTC Token Functionality
1. Sending ckBTC

Implement functionality for the user to send ckBTC via the ledger canister mxzaz-hqaaa-aaaar-qaada-cai.

Note:
where the transfer payload includes a BigInt value directly (e.g. { amount: BigInt(value) }).
Since the Internet Computer agent serializes arguments via Candid, it cannot encode raw BigInts into JSON.
Convert the amount to a string or numeric literal before dispatch — for example using amount: value.toString() or Number(value) if precision allows.

2. Displaying Balance
To retrieve the balance, call the ledger canister method:

icrc1_balance_of : (record { owner: principal; subaccount: opt vec nat8 }) → (nat) query.
icrc1_decimals: () → (nat8) query
Do not include the optional subaccount parameter in icrc1_balance_of.
Use the returned nat value and convert it to ckBTC by using conversion.


Precision must not be lost
Use compatible types during:

ledger call

conversion

formatting

UI display

Balance Retrieval (pseudocode reference)
export function useGetTokenBalance(token: Token | null) {
  const { identity } = useInternetIdentity();

  return useQuery<bigint>({
    queryKey: ['tokenBalance', token?.symbol, identity?.getPrincipal().toString()],
    queryFn: async () => {
      if (!identity || !token) throw new Error('Identity or token not available');
      
      const account = createAccount(identity.getPrincipal());
      
      // For ICP, use the index canister's icrc1_balance_of method
      if (token.symbol === 'ICP' && token.index_canister_id) {
        const indexActor = await createICPIndexActor(token.index_canister_id, identity);
        const balance = await indexActor.icrc1_balance_of(account);
        // Convert nat64 to bigint
        return BigInt(balance.toString());
      }
      
      // For other tokens, use the ledger canister
      const ledger = await createGenericLedgerActor(token.ledger_canister_id, identity);
      const balance = await ledger.icrc1_balance_of(account);
      
      // Return raw balance without normalization
      return balance;
    },
    enabled: !!identity && !!token,
    refetchInterval: 10000,
  });
}




Summary of Requirements

Implement:

transfer functionality

balance retrieval with icrc1_balance_of()

Balance conversion

Display balance with 8-decimal precision

No precision loss in type handling or formatting

Before you begin, what questions do you have?

3 Likes

hey webtree, why are all your prompts CLIs, no one likes those.

Because caffeine gets them correct on the first try, along with the logic that’s why.

You can always change the UI after the underlying logic is correct.

2 Likes

Thank you very much!

1 Like

obligatory, this one is for @Lorimer
icpwizard.com

1 Like