Skip to main content
This guide shows how to connect ethers.js or viem to StableNet Testnet, query balances, send transactions with the required priority fee, and interact with the WKRC ERC-20 contract. Tools: ethers.js v6 or viem Network: StableNet Testnet (Chain ID 8283)

Prerequisites

  • Node.js ≥ 18 installed
  • A funded testnet wallet — get free WKRC from the faucet:

    Faucet

    Request testnet WKRC — no sign-up required.

    Explorer

    Inspect transactions and balances on-chain.
Every transaction on StableNet must include maxPriorityFeePerGas of at least 27,600 Gwei (27600000000000 wei). Transactions below this threshold are rejected at the transaction pool level. You must also set maxFeePerGas — omitting it defaults to 0 and causes rejection.

Network Details

ParameterValue
Chain ID8283
RPC URLhttps://api.test.stablenet.network
WKRC Contract0x0000000000000000000000000000000000001000
Minimum maxPriorityFeePerGas27,600 Gwei (27600000000000 wei)
eth_maxPriorityFeePerGasReturns the governance-enforced value, not a fee estimate
WKRC is the native gas coin of StableNet exposed as a standard ERC-20 token via the NativeCoinAdapter system contract. The same balance used to pay gas is the balance returned by balanceOf() — no wrapping required.

ethers.js

Install

npm install ethers dotenv

Connect to StableNet

import { ethers } from "ethers";
import "dotenv/config";

const RPC_URL = "https://api.test.stablenet.network";

const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

// Verify connection
const network = await provider.getNetwork();
console.log("Chain ID:", network.chainId.toString()); // 8283
On Node.js v20+, you can use the --env-file=.env flag instead of dotenv: node --env-file=.env your-script.js

Query balances

const ADDRESS = wallet.address;

// Native WKRC balance (same as gas balance)
const nativeBalance = await provider.getBalance(ADDRESS);
console.log("Native WKRC:", ethers.formatEther(nativeBalance));

// WKRC via NativeCoinAdapter ERC-20 interface
const WKRC_ADDRESS = "0x0000000000000000000000000000000000001000";
const ERC20_ABI = [
  "function balanceOf(address) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function transfer(address to, uint256 amount) returns (bool)",
];

const wkrc = new ethers.Contract(WKRC_ADDRESS, ERC20_ABI, wallet);
const erc20Balance = await wkrc.balanceOf(ADDRESS);
console.log("WKRC (ERC-20):", ethers.formatEther(erc20Balance));
// Native balance and ERC-20 balance are always equal

Send a transaction

// Always set both maxPriorityFeePerGas and maxFeePerGas
const tx = await wallet.sendTransaction({
  to: "0xRecipientAddress",
  value: ethers.parseEther("1.0"),
  maxPriorityFeePerGas: ethers.parseUnits("27600", "gwei"),
  maxFeePerGas: ethers.parseUnits("80000", "gwei"),
});

const receipt = await tx.wait();
console.log("Confirmed in block:", receipt.blockNumber);
tx.wait() polls eth_getTransactionReceipt until the transaction is included. Block time is ~1 second, but allow up to 2 minutes for mempool propagation (field-tested; actual time varies). Do not cancel and retry — that may cause a nonce conflict.

Approve WKRC for a contract

const CONTRACT_ADDRESS = "0xYourContractAddress";
const AMOUNT = ethers.parseEther("1.0");

// Approve the contract to spend 1 WKRC
const approveTx = await wkrc.approve(CONTRACT_ADDRESS, AMOUNT, {
  maxPriorityFeePerGas: ethers.parseUnits("27600", "gwei"),
  maxFeePerGas: ethers.parseUnits("80000", "gwei"),
});
await approveTx.wait();
console.log("Approval confirmed");

// Verify allowance
const allowance = await wkrc.allowance(wallet.address, CONTRACT_ADDRESS);
console.log("Allowance:", ethers.formatEther(allowance));

Call a contract function

const PAYMENT_ABI = [
  "function pay(address to, uint256 amount) external",
  "function allowanceOf(address owner) view returns (uint256)",
];

const paymentContract = new ethers.Contract(
  CONTRACT_ADDRESS,
  PAYMENT_ABI,
  wallet
);

// Read — no fee needed for view calls
const remaining = await paymentContract.allowanceOf(wallet.address);
console.log("Remaining allowance:", ethers.formatEther(remaining));

// Write — always include fee params
const payTx = await paymentContract.pay(
  "0xRecipientAddress",
  ethers.parseEther("1.0"),
  {
    maxPriorityFeePerGas: ethers.parseUnits("27600", "gwei"),
    maxFeePerGas: ethers.parseUnits("80000", "gwei"),
  }
);
await payTx.wait();
console.log("Payment confirmed");

Query the enforced priority fee

// Returns the governance-controlled minimum, not a market estimate
const tip = await provider.send("eth_maxPriorityFeePerGas", []);
console.log("Enforced tip:", BigInt(tip).toString(), "wei");
// e.g. 27600000000000

viem

Install

npm install viem dotenv

Define the chain and clients

Define the StableNet Testnet chain object once and reuse it across all clients:
import {
  createPublicClient,
  createWalletClient,
  http,
  parseEther,
  parseGwei,
  formatEther,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import "dotenv/config";

const stablenetTestnet = {
  id: 8283,
  name: "StableNet Testnet",
  nativeCurrency: { name: "WKRC", symbol: "WKRC", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://api.test.stablenet.network"] },
  },
  blockExplorers: {
    default: {
      name: "StableNet Explorer",
      url: "https://explorer.stablenet.network",
    },
  },
};

const publicClient = createPublicClient({
  chain: stablenetTestnet,
  transport: http(),
});

const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
const walletClient = createWalletClient({
  chain: stablenetTestnet,
  transport: http(),
  account,
});
On Node.js v20+, you can use the --env-file=.env flag instead of dotenv: node --env-file=.env your-script.js

Query balances

const ADDRESS = account.address;

// Native WKRC balance
const nativeBalance = await publicClient.getBalance({ address: ADDRESS });
console.log("Native WKRC:", formatEther(nativeBalance));

// WKRC via NativeCoinAdapter ERC-20 interface
const WKRC_ADDRESS = "0x0000000000000000000000000000000000001000";
const ERC20_ABI = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "allowance",
    type: "function",
    stateMutability: "view",
    inputs: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
    ],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "approve",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "", type: "bool" }],
  },
];

const erc20Balance = await publicClient.readContract({
  address: WKRC_ADDRESS,
  abi: ERC20_ABI,
  functionName: "balanceOf",
  args: [ADDRESS],
});
console.log("WKRC (ERC-20):", formatEther(erc20Balance));

Send a transaction

// Always set both maxPriorityFeePerGas and maxFeePerGas
const hash = await walletClient.sendTransaction({
  to: "0xRecipientAddress",
  value: parseEther("1.0"),
  maxPriorityFeePerGas: parseGwei("27600"),
  maxFeePerGas: parseGwei("80000"),
});

const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Confirmed in block:", receipt.blockNumber);

Approve WKRC for a contract

const CONTRACT_ADDRESS = "0xYourContractAddress";

// Approve
const approveHash = await walletClient.writeContract({
  address: WKRC_ADDRESS,
  abi: ERC20_ABI,
  functionName: "approve",
  args: [CONTRACT_ADDRESS, parseEther("1.0")],
  maxPriorityFeePerGas: parseGwei("27600"),
  maxFeePerGas: parseGwei("80000"),
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });

// Verify allowance
const allowance = await publicClient.readContract({
  address: WKRC_ADDRESS,
  abi: ERC20_ABI,
  functionName: "allowance",
  args: [account.address, CONTRACT_ADDRESS],
});
console.log("Allowance:", formatEther(allowance));

Call a contract function

const PAYMENT_ABI = [
  {
    name: "pay",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "allowanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
];

// Read — no fee needed for view calls
const remaining = await publicClient.readContract({
  address: CONTRACT_ADDRESS,
  abi: PAYMENT_ABI,
  functionName: "allowanceOf",
  args: [account.address],
});
console.log("Remaining allowance:", formatEther(remaining));

// Write — always include fee params
const payHash = await walletClient.writeContract({
  address: CONTRACT_ADDRESS,
  abi: PAYMENT_ABI,
  functionName: "pay",
  args: ["0xRecipientAddress", parseEther("1.0")],
  maxPriorityFeePerGas: parseGwei("27600"),
  maxFeePerGas: parseGwei("80000"),
});
await publicClient.waitForTransactionReceipt({ hash: payHash });
console.log("Payment confirmed");

Query the enforced priority fee

// Returns the governance-controlled minimum, not a market estimate
const tip = await publicClient.request({
  method: "eth_maxPriorityFeePerGas",
});
console.log("Enforced tip:", BigInt(tip).toString(), "wei");
// e.g. 27600000000000

Common Notes

Priority fee on every transaction

The GovValidator governance contract enforces a network-wide minimum maxPriorityFeePerGas on every transaction type — native transfers, contract deploys, and contract calls. Omitting it causes immediate rejection.
FieldMinimumReference when slow
maxPriorityFeePerGas27,600 Gwei35,000 Gwei
maxFeePerGasMust be ≥ maxPriorityFeePerGas80,000 Gwei
The 35,000 Gwei / 80,000 Gwei values above are based on StableNet Testnet field testing. They are not official protocol specifications — adjust based on observed mempool conditions.
eth_maxPriorityFeePerGas on StableNet returns the governance-enforced value from GovValidator, not an oracle estimate based on recent blocks. You can use it to fetch the current minimum programmatically.

WKRC is native and ERC-20 simultaneously

The NativeCoinAdapter at 0x0000000000000000000000000000000000001000 exposes the native gas coin as a standard ERC-20 token. You do not need to wrap it:
  • provider.getBalance() / publicClient.getBalance() — returns the same value as balanceOf()
  • Standard approve / transferFrom patterns work exactly as on Ethereum
  • transfer() on the WKRC contract moves the same balance used for gas

Troubleshooting

  • Transaction rejected / “fee too low” — set both maxPriorityFeePerGas (min 27,600 Gwei) and maxFeePerGas on every send.
  • maxFeePerGas defaults to 0 — always specify it explicitly alongside maxPriorityFeePerGas. Omitting it causes rejection even if the priority fee is correct.
  • Slow confirmation — increase to maxPriorityFeePerGas: parseGwei("35000") and maxFeePerGas: parseGwei("80000") (field-tested reference values, not official specs).
  • tx.wait() / waitForTransactionReceipt hangs — the transaction is in the mempool. Wait up to 2 minutes (field-tested; actual time varies); do not cancel and retry with the same nonce.
  • “replacement transaction underpriced” — raise both fee fields to replace a pending transaction with the same nonce.

Next Steps

Foundry Quickstart

Deploy a WKRC payment contract with Foundry — includes approve and pay flow.

Hardhat Guide

Deploy contracts with Hardhat and call them using ethers.js.

EVM Compatibility Reference

Supported opcodes, tool snippets, fee delegation (tx type 0x16), and unsupported features.

Explorer

Inspect transactions and contracts on-chain.

Example Contracts

Foundry, Hardhat, ethers.js, and viem example collection.