ERCs

ERC-20 — Fungible Token Standard

eips.ethereum.org/EIPS/eip-20

import {
  balanceOf,
  totalSupply,
  transfer,
  approve,
  allowance,
  transferFrom,
} from "@ethernauta/erc/20";

Core methods

MethodShapePurpose
nameCallable<string>Token name.
symbolCallable<string>Token symbol.
decimalsCallable<number>Decimals exponent.
totalSupplyCallable<Uint256>Total supply.
balanceOf({ owner })Callable<Uint256>Balance of an account.
allowance({ owner, spender })Callable<Uint256>Approval amount.
transfer({ to, amount })Signable<Hash32>Send tokens.
approve({ spender, amount })Signable<Hash32>Grant allowance.
transferFrom({ from, to, amount })Signable<Hash32>Transfer using allowance.

Reading a balance

import {
  contract,
  create_reader,
  encode_chain_id,
  http,
} from "@ethernauta/transport";
import { balanceOf, decimals } from "@ethernauta/erc/20";
import { eth_call } from "@ethernauta/eth";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { AddressSchema, type Bytes } from "@ethernauta/core";
import { format_unit } from "@ethernauta/utils";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);

const usdc_address = parse(AddressSchema, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const holder = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");

const ctx = contract({ chain_id: CHAIN_ID, to: usdc_address });

const balance_call = balanceOf([holder])(ctx);
const balance_bytes: Bytes = await eth_call([{ to: balance_call.to, input: balance_call.data }])(
  reader({ chain_id: CHAIN_ID }),
);
const raw = balance_call.decode(balance_bytes);

const decimals_call = decimals()(ctx);
const decimals_bytes: Bytes = await eth_call([{ to: decimals_call.to, input: decimals_call.data }])(
  reader({ chain_id: CHAIN_ID }),
);
const d = decimals_call.decode(decimals_bytes);

console.log(format_unit(BigInt(raw), Number(BigInt(d))), "USDC");

Transferring

import { transfer } from "@ethernauta/erc/20";
import type { Provider } from "@ethernauta/eip/1193";
import { create_provider, encode_chain_id } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { AddressSchema, Uint256Schema } from "@ethernauta/core";
import { parse_unit } from "@ethernauta/utils";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);

const usdc_address = parse(AddressSchema, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const recipient = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const value = parse(Uint256Schema, `0x${parse_unit("100", 6).toString(16)}`);

const signed = await transfer([recipient, value])(
  signer({ chain_id: CHAIN_ID, to: usdc_address }),
);

void signed;

Wallet handles nonce + gas; to is the token contract, not the recipient. The to recipient is in the call arguments.

Extensions

ERC-20 has several optional extensions, each at a sub-subpath:

SubpathMethods
@ethernauta/erc/20/burnableburn, burn_from
@ethernauta/erc/20/cappedcap
@ethernauta/erc/20/metadataname, symbol, decimals (re-export)
@ethernauta/erc/20/mintablemint
@ethernauta/erc/20/pausablepause, unpause, paused
@ethernauta/erc/20/wrapperdeposit_for, withdraw_to (wrapped-token pattern)

Import only what you need; tree-shaking removes the rest.

See also