Merge dex-devnet3 d5dd6fa1 into master (gate: green on d5dd6fa1, recorded by tools/ci/pre-push.sh; landed on the build mirror)

This commit is contained in:
igneum-labs 2026-10-08 10:32:52 +00:00
commit cd190d13c9
38 changed files with 1436 additions and 3 deletions

View file

@ -0,0 +1,19 @@
[profile.default]
src = "src"
test = "test"
script = "script"
out = "out"
libs = []
solc_version = "0.8.28"
# Devnet 3's executor runs the EVM through revm; Paris keeps the bytecode off PUSH0 and transient storage so it runs
# on any post-Merge configuration of it (8 October 2026).
evm_version = "paris"
optimizer = true
optimizer_runs = 200
fs_permissions = [{ access = "read-write", path = "./deploy-out.json" }]
# no remote dependencies: the tests and the script declare the cheatcode interface they use (test/Vm.sol); solc 0.8.28 is fetched once by forge
auto_detect_remappings = false
[rpc_endpoints]
# the Devnet 3 EVM reaches the build box through a tunnel on this port (never the Mac); see docs/contracts/devnet-3.json
devnet3 = "http://127.0.0.1:36790"

View file

@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Vm, VM_ADDRESS} from "../test/Vm.sol";
import {WIGN} from "../src/WIGN.sol";
import {TestToken} from "../src/TestToken.sol";
import {IgneumFactory} from "../src/IgneumPair.sol";
import {IgneumRouter} from "../src/IgneumRouter.sol";
/// Deploys the AMM on Devnet 3 from the key in DEX_DEPLOYER_KEY (read from the environment, never printed), seeds
/// three pools from the faucet and 200 IGN, and writes the addresses to deploy-out.json for docs/contracts.
///
/// forge script script/Deploy.s.sol:Deploy --rpc-url devnet3 --broadcast --sig "run()"
///
/// Devnet 3, test tokens, no value.
contract Deploy {
Vm constant vm = Vm(VM_ADDRESS);
function run() external {
uint256 key = vm.envUint("DEX_DEPLOYER_KEY");
address deployer = vm.addr(key);
vm.startBroadcast(key);
WIGN wign = new WIGN();
TestToken tta = new TestToken("Test Token A", "TTA");
TestToken ttb = new TestToken("Test Token B", "TTB");
IgneumFactory factory = new IgneumFactory();
IgneumRouter router = new IgneumRouter(address(factory), address(wign));
tta.drip();
ttb.drip();
tta.approve(address(router), type(uint256).max);
ttb.approve(address(router), type(uint256).max);
uint256 deadline = block.timestamp + 1 hours;
router.addLiquidityIGN{value: 100 ether}(address(tta), 500 ether, 0, 0, deployer, deadline);
router.addLiquidityIGN{value: 100 ether}(address(ttb), 500 ether, 0, 0, deployer, deadline);
router.addLiquidity(address(tta), address(ttb), 500 ether, 500 ether, 0, 0, deployer, deadline);
vm.stopBroadcast();
string memory json = "{\n";
json = _line(json, "WIGN", address(wign), ",");
json = _line(json, "TTA", address(tta), ",");
json = _line(json, "TTB", address(ttb), ",");
json = _line(json, "IgneumFactory", address(factory), ",");
json = _line(json, "IgneumRouter", address(router), ",");
json = _line(json, "pair_TTA_WIGN", factory.getPair(address(tta), address(wign)), ",");
json = _line(json, "pair_TTB_WIGN", factory.getPair(address(ttb), address(wign)), ",");
json = _line(json, "pair_TTA_TTB", factory.getPair(address(tta), address(ttb)), ",");
json = _line(json, "deployer", deployer, "");
json = string.concat(json, "}\n");
vm.writeFile("deploy-out.json", json);
}
function _line(string memory json, string memory key, address value, string memory comma) private pure returns (string memory) {
return string.concat(json, " \"", key, "\": \"", vm.toString(value), "\"", comma, "\n");
}
}

View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Vm, VM_ADDRESS} from "../test/Vm.sol";
import {TestToken} from "../src/TestToken.sol";
import {IgneumRouter} from "../src/IgneumRouter.sol";
/// Seeds the three pools of an already deployed AMM (the addresses from the environment): a drip of each test
/// token, approvals, 100 IGN beside 500 TTA, 100 IGN beside 500 TTB, 500 TTA beside 500 TTB. Run with
/// --skip-simulation so every gas limit comes from the node's own eth_estimateGas: Devnet 3 charges the proving
/// dimension inside the execution gas, so Foundry's local estimate runs out (8 October 2026, drip() at 133,603 gas:
/// out of gas; the node's estimate 685,513).
///
/// DEX_TTA=0x.. DEX_TTB=0x.. DEX_ROUTER=0x.. forge script script/Seed.s.sol:Seed --rpc-url devnet3 --broadcast --slow --skip-simulation --sig "run()"
///
/// Devnet 3, test tokens, no value.
contract Seed {
Vm constant vm = Vm(VM_ADDRESS);
function run() external {
uint256 key = vm.envUint("DEX_DEPLOYER_KEY");
address deployer = vm.addr(key);
TestToken tta = TestToken(vm.envAddress("DEX_TTA"));
TestToken ttb = TestToken(vm.envAddress("DEX_TTB"));
IgneumRouter router = IgneumRouter(payable(vm.envAddress("DEX_ROUTER")));
vm.startBroadcast(key);
if (tta.dripWait(deployer) == 0 && tta.balanceOf(deployer) < 1_000 ether) tta.drip();
if (ttb.dripWait(deployer) == 0 && ttb.balanceOf(deployer) < 1_000 ether) ttb.drip();
tta.approve(address(router), type(uint256).max);
ttb.approve(address(router), type(uint256).max);
uint256 deadline = block.timestamp + 1 hours;
router.addLiquidityIGN{value: 100 ether}(address(tta), 500 ether, 0, 0, deployer, deadline);
router.addLiquidityIGN{value: 100 ether}(address(ttb), 500 ether, 0, 0, deployer, deadline);
router.addLiquidity(address(tta), address(ttb), 500 ether, 500 ether, 0, 0, deployer, deadline);
vm.stopBroadcast();
}
}

View file

@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// A plain ERC-20 with EIP-2612-free approvals, shared by the pair's liquidity token, the wrapped coin and the test
/// tokens. Devnet 3, test tokens, no value.
contract ERC20 {
string public name;
string public symbol;
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}
function _mint(address to, uint256 value) internal {
totalSupply += value;
balanceOf[to] += value;
emit Transfer(address(0), to, value);
}
function _burn(address from, uint256 value) internal {
balanceOf[from] -= value;
totalSupply -= value;
emit Transfer(from, address(0), value);
}
function _transfer(address from, address to, uint256 value) internal {
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
}
function approve(address spender, uint256 value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transfer(address to, uint256 value) external returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - value;
}
_transfer(from, to, value);
return true;
}
}

View file

@ -0,0 +1,174 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "./ERC20.sol";
interface IERC20Minimal {
function balanceOf(address) external view returns (uint256);
function transfer(address, uint256) external returns (bool);
}
/// A constant-product pool in the Uniswap v2 shape: reserves of two tokens, a 0.3 percent fee kept in the pool,
/// liquidity tokens for the depositors, MINIMUM_LIQUIDITY locked at the first mint. No protocol fee, no price
/// accumulators, no flash callback. Devnet 3, test tokens, no value.
contract IgneumPair is ERC20("Igneum LP", "IGN-LP") {
uint256 public constant MINIMUM_LIQUIDITY = 10 ** 3;
address public immutable factory;
address public token0;
address public token1;
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
uint256 private unlocked = 1;
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to);
event Sync(uint112 reserve0, uint112 reserve1);
modifier lock() {
require(unlocked == 1, "Pair: locked");
unlocked = 0;
_;
unlocked = 1;
}
constructor() {
factory = msg.sender;
}
function initialize(address token0_, address token1_) external {
require(msg.sender == factory, "Pair: forbidden");
token0 = token0_;
token1 = token1_;
}
function getReserves() public view returns (uint112, uint112, uint32) {
return (reserve0, reserve1, blockTimestampLast);
}
function _safeTransfer(address token, address to, uint256 value) private {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, value));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "Pair: transfer failed");
}
function _update(uint256 balance0, uint256 balance1) private {
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "Pair: overflow");
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = uint32(block.timestamp);
emit Sync(reserve0, reserve1);
}
function _sqrt(uint256 y) private pure returns (uint256 z) {
if (y > 3) {
z = y;
uint256 x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
function _min(uint256 x, uint256 y) private pure returns (uint256) {
return x < y ? x : y;
}
/// Mints liquidity for the tokens sent to the pair since the last sync. Called by the router.
function mint(address to) external lock returns (uint256 liquidity) {
(uint112 r0, uint112 r1,) = getReserves();
uint256 balance0 = IERC20Minimal(token0).balanceOf(address(this));
uint256 balance1 = IERC20Minimal(token1).balanceOf(address(this));
uint256 amount0 = balance0 - r0;
uint256 amount1 = balance1 - r1;
if (totalSupply == 0) {
liquidity = _sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0xdead), MINIMUM_LIQUIDITY);
} else {
liquidity = _min(amount0 * totalSupply / r0, amount1 * totalSupply / r1);
}
require(liquidity > 0, "Pair: insufficient liquidity minted");
_mint(to, liquidity);
_update(balance0, balance1);
emit Mint(msg.sender, amount0, amount1);
}
/// Burns the liquidity tokens sent to the pair and pays both tokens out pro rata. Called by the router.
function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
uint256 balance0 = IERC20Minimal(token0).balanceOf(address(this));
uint256 balance1 = IERC20Minimal(token1).balanceOf(address(this));
uint256 liquidity = balanceOf[address(this)];
amount0 = liquidity * balance0 / totalSupply;
amount1 = liquidity * balance1 / totalSupply;
require(amount0 > 0 && amount1 > 0, "Pair: insufficient liquidity burned");
_burn(address(this), liquidity);
_safeTransfer(token0, to, amount0);
_safeTransfer(token1, to, amount1);
_update(IERC20Minimal(token0).balanceOf(address(this)), IERC20Minimal(token1).balanceOf(address(this)));
emit Burn(msg.sender, amount0, amount1, to);
}
/// Pays out up to the amounts asked and checks the fee-adjusted product did not fall. The input must already
/// sit in the pair (the router sends it first).
function swap(uint256 amount0Out, uint256 amount1Out, address to) external lock {
require(amount0Out > 0 || amount1Out > 0, "Pair: insufficient output amount");
(uint112 r0, uint112 r1,) = getReserves();
require(amount0Out < r0 && amount1Out < r1, "Pair: insufficient liquidity");
require(to != token0 && to != token1, "Pair: invalid to");
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
uint256 balance0 = IERC20Minimal(token0).balanceOf(address(this));
uint256 balance1 = IERC20Minimal(token1).balanceOf(address(this));
uint256 amount0In = balance0 > r0 - amount0Out ? balance0 - (r0 - amount0Out) : 0;
uint256 amount1In = balance1 > r1 - amount1Out ? balance1 - (r1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, "Pair: insufficient input amount");
uint256 adjusted0 = balance0 * 1000 - amount0In * 3;
uint256 adjusted1 = balance1 * 1000 - amount1In * 3;
require(adjusted0 * adjusted1 >= uint256(r0) * uint256(r1) * 1000 ** 2, "Pair: K");
_update(balance0, balance1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
/// Sends any balance above the reserves to `to`.
function skim(address to) external lock {
_safeTransfer(token0, to, IERC20Minimal(token0).balanceOf(address(this)) - reserve0);
_safeTransfer(token1, to, IERC20Minimal(token1).balanceOf(address(this)) - reserve1);
}
/// Sets the reserves to the balances.
function sync() external lock {
_update(IERC20Minimal(token0).balanceOf(address(this)), IERC20Minimal(token1).balanceOf(address(this)));
}
}
/// Creates one pair per unordered token pair and remembers it.
contract IgneumFactory {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(address indexed token0, address indexed token1, address pair, uint256 count);
function allPairsLength() external view returns (uint256) {
return allPairs.length;
}
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, "Factory: identical addresses");
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), "Factory: zero address");
require(getPair[token0][token1] == address(0), "Factory: pair exists");
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
pair = address(new IgneumPair{salt: salt}());
IgneumPair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
}

View file

@ -0,0 +1,253 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IgneumFactory, IgneumPair} from "./IgneumPair.sol";
interface IERC20Router {
function balanceOf(address) external view returns (uint256);
function transfer(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}
interface IWIGN is IERC20Router {
function deposit() external payable;
function withdraw(uint256) external;
}
/// The router in the Uniswap v2 shape: adds and removes liquidity, swaps along a path of pairs, quotes. Pairs are
/// looked up on the factory, never derived from an init-code hash. Devnet 3, test tokens, no value.
contract IgneumRouter {
IgneumFactory public immutable factory;
address public immutable WIGN;
modifier ensure(uint256 deadline) {
require(deadline >= block.timestamp, "Router: expired");
_;
}
constructor(address factory_, address wign_) {
factory = IgneumFactory(factory_);
WIGN = wign_;
}
receive() external payable {
require(msg.sender == WIGN, "Router: only WIGN");
}
// ---- pure maths ----
function sortTokens(address tokenA, address tokenB) public pure returns (address token0, address token1) {
require(tokenA != tokenB, "Router: identical addresses");
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), "Router: zero address");
}
function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) public pure returns (uint256 amountB) {
require(amountA > 0, "Router: insufficient amount");
require(reserveA > 0 && reserveB > 0, "Router: insufficient liquidity");
amountB = amountA * reserveB / reserveA;
}
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountOut) {
require(amountIn > 0, "Router: insufficient input amount");
require(reserveIn > 0 && reserveOut > 0, "Router: insufficient liquidity");
uint256 amountInWithFee = amountIn * 997;
amountOut = amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee);
}
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountIn) {
require(amountOut > 0, "Router: insufficient output amount");
require(reserveIn > 0 && reserveOut > 0, "Router: insufficient liquidity");
amountIn = (reserveIn * amountOut * 1000) / ((reserveOut - amountOut) * 997) + 1;
}
// ---- views ----
function pairFor(address tokenA, address tokenB) public view returns (address pair) {
pair = factory.getPair(tokenA, tokenB);
require(pair != address(0), "Router: no pair");
}
function getReserves(address tokenA, address tokenB) public view returns (uint256 reserveA, uint256 reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint112 r0, uint112 r1,) = IgneumPair(pairFor(tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (r0, r1) : (r1, r0);
}
function getAmountsOut(uint256 amountIn, address[] memory path) public view returns (uint256[] memory amounts) {
require(path.length >= 2, "Router: invalid path");
amounts = new uint256[](path.length);
amounts[0] = amountIn;
for (uint256 i; i < path.length - 1; i++) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
function getAmountsIn(uint256 amountOut, address[] memory path) public view returns (uint256[] memory amounts) {
require(path.length >= 2, "Router: invalid path");
amounts = new uint256[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint256 i = path.length - 1; i > 0; i--) {
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
// ---- liquidity ----
function _addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin)
private
returns (uint256 amountA, uint256 amountB)
{
if (factory.getPair(tokenA, tokenB) == address(0)) {
factory.createPair(tokenA, tokenB);
}
(uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, "Router: insufficient B amount");
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
require(amountAOptimal <= amountADesired && amountAOptimal >= amountAMin, "Router: insufficient A amount");
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = pairFor(tokenA, tokenB);
_pull(tokenA, msg.sender, pair, amountA);
_pull(tokenB, msg.sender, pair, amountB);
liquidity = IgneumPair(pair).mint(to);
}
function addLiquidityIGN(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountIGNMin, address to, uint256 deadline)
external
payable
ensure(deadline)
returns (uint256 amountToken, uint256 amountIGN, uint256 liquidity)
{
(amountToken, amountIGN) = _addLiquidity(token, WIGN, amountTokenDesired, msg.value, amountTokenMin, amountIGNMin);
address pair = pairFor(token, WIGN);
_pull(token, msg.sender, pair, amountToken);
IWIGN(WIGN).deposit{value: amountIGN}();
require(IWIGN(WIGN).transfer(pair, amountIGN), "Router: WIGN transfer failed");
liquidity = IgneumPair(pair).mint(to);
if (msg.value > amountIGN) _sendIGN(msg.sender, msg.value - amountIGN);
}
function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)
public
ensure(deadline)
returns (uint256 amountA, uint256 amountB)
{
address pair = pairFor(tokenA, tokenB);
_pull(pair, msg.sender, pair, liquidity);
(uint256 amount0, uint256 amount1) = IgneumPair(pair).burn(to);
(address token0,) = sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, "Router: insufficient A amount");
require(amountB >= amountBMin, "Router: insufficient B amount");
}
function removeLiquidityIGN(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountIGNMin, address to, uint256 deadline)
external
ensure(deadline)
returns (uint256 amountToken, uint256 amountIGN)
{
(amountToken, amountIGN) = removeLiquidity(token, WIGN, liquidity, amountTokenMin, amountIGNMin, address(this), deadline);
require(IERC20Router(token).transfer(to, amountToken), "Router: token transfer failed");
IWIGN(WIGN).withdraw(amountIGN);
_sendIGN(to, amountIGN);
}
// ---- swaps ----
function _swap(uint256[] memory amounts, address[] memory path, address to_) private {
for (uint256 i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = sortTokens(input, output);
uint256 amountOut = amounts[i + 1];
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
address to = i < path.length - 2 ? pairFor(output, path[i + 2]) : to_;
IgneumPair(pairFor(input, output)).swap(amount0Out, amount1Out, to);
}
}
function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
external
ensure(deadline)
returns (uint256[] memory amounts)
{
amounts = getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, "Router: insufficient output amount");
_pull(path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]);
_swap(amounts, path, to);
}
function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline)
external
ensure(deadline)
returns (uint256[] memory amounts)
{
amounts = getAmountsIn(amountOut, path);
require(amounts[0] <= amountInMax, "Router: excessive input amount");
_pull(path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]);
_swap(amounts, path, to);
}
function swapExactIGNForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
external
payable
ensure(deadline)
returns (uint256[] memory amounts)
{
require(path[0] == WIGN, "Router: invalid path");
amounts = getAmountsOut(msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, "Router: insufficient output amount");
IWIGN(WIGN).deposit{value: amounts[0]}();
require(IWIGN(WIGN).transfer(pairFor(path[0], path[1]), amounts[0]), "Router: WIGN transfer failed");
_swap(amounts, path, to);
}
function swapExactTokensForIGN(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
external
ensure(deadline)
returns (uint256[] memory amounts)
{
require(path[path.length - 1] == WIGN, "Router: invalid path");
amounts = getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, "Router: insufficient output amount");
_pull(path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]);
_swap(amounts, path, address(this));
IWIGN(WIGN).withdraw(amounts[amounts.length - 1]);
_sendIGN(to, amounts[amounts.length - 1]);
}
// ---- transfers ----
function _pull(address token, address from, address to, uint256 value) private {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Router.transferFrom.selector, from, to, value));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "Router: transferFrom failed");
}
function _sendIGN(address to, uint256 value) private {
(bool ok,) = to.call{value: value}("");
require(ok, "Router: IGN send failed");
}
}

View file

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "./ERC20.sol";
/// A test token with its own faucet: anyone takes DRIP tokens once an hour. Devnet 3, test tokens, no value.
contract TestToken is ERC20 {
uint256 public constant DRIP = 1_000 ether;
uint256 public constant DRIP_INTERVAL = 1 hours;
mapping(address => uint256) public lastDrip;
event Drip(address indexed to, uint256 value);
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
/// Mints DRIP tokens to the caller; refused inside DRIP_INTERVAL of the caller's last drip.
function drip() external {
uint256 last = lastDrip[msg.sender];
require(last == 0 || block.timestamp >= last + DRIP_INTERVAL, "TestToken: one drip an hour");
lastDrip[msg.sender] = block.timestamp;
_mint(msg.sender, DRIP);
emit Drip(msg.sender, DRIP);
}
/// Seconds until the caller may drip again (0 when it may).
function dripWait(address who) external view returns (uint256) {
uint256 last = lastDrip[who];
if (last == 0) return 0;
uint256 next = last + DRIP_INTERVAL;
return block.timestamp >= next ? 0 : next - block.timestamp;
}
}

View file

@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "./ERC20.sol";
/// Wrapped IGN, the WETH9 shape: one WIGN per IGN held by this contract, minted on deposit and burned on withdraw.
/// Devnet 3, test tokens, no value.
contract WIGN is ERC20("Wrapped IGN", "WIGN") {
event Deposit(address indexed to, uint256 value);
event Withdrawal(address indexed from, uint256 value);
receive() external payable {
deposit();
}
function deposit() public payable {
_mint(msg.sender, msg.value);
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 value) external {
_burn(msg.sender, value);
emit Withdrawal(msg.sender, value);
(bool ok,) = msg.sender.call{value: value}("");
require(ok, "WIGN: send failed");
}
}

View file

@ -0,0 +1,137 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Vm, VM_ADDRESS} from "./Vm.sol";
import {WIGN} from "../src/WIGN.sol";
import {TestToken} from "../src/TestToken.sol";
import {IgneumFactory, IgneumPair} from "../src/IgneumPair.sol";
import {IgneumRouter} from "../src/IgneumRouter.sol";
/// The AMM end to end on Foundry's EVM: pair creation, liquidity in and out, token and IGN swaps, the faucet's
/// hour, the k check. Every number is a test number: Devnet 3, test tokens, no value.
contract DexTest {
Vm constant vm = Vm(VM_ADDRESS);
WIGN wign;
TestToken tta;
TestToken ttb;
IgneumFactory factory;
IgneumRouter router;
address alice = address(0xA11CE);
address bob = address(0xB0B);
receive() external payable {}
function setUp() public {
wign = new WIGN();
tta = new TestToken("Test Token A", "TTA");
ttb = new TestToken("Test Token B", "TTB");
factory = new IgneumFactory();
router = new IgneumRouter(address(factory), address(wign));
vm.deal(alice, 1_000 ether);
vm.deal(bob, 1_000 ether);
}
function _drip(address who) internal {
vm.startPrank(who);
tta.drip();
ttb.drip();
tta.approve(address(router), type(uint256).max);
ttb.approve(address(router), type(uint256).max);
vm.stopPrank();
}
function _seed() internal returns (address pairAB, address pairAW) {
_drip(alice);
vm.startPrank(alice);
router.addLiquidity(address(tta), address(ttb), 500 ether, 500 ether, 0, 0, alice, block.timestamp + 60);
router.addLiquidityIGN{value: 100 ether}(address(tta), 500 ether, 0, 0, alice, block.timestamp + 60);
vm.stopPrank();
pairAB = factory.getPair(address(tta), address(ttb));
pairAW = factory.getPair(address(tta), address(wign));
}
function test_faucet_drips_once_an_hour() public {
vm.prank(bob);
tta.drip();
require(tta.balanceOf(bob) == 1_000 ether, "drip amount");
vm.prank(bob);
vm.expectRevert(bytes("TestToken: one drip an hour"));
tta.drip();
vm.warp(block.timestamp + 1 hours);
vm.prank(bob);
tta.drip();
require(tta.balanceOf(bob) == 2_000 ether, "second drip");
}
function test_add_liquidity_creates_pairs_and_mints() public {
(address pairAB, address pairAW) = _seed();
require(pairAB != address(0) && pairAW != address(0) && pairAB != pairAW, "pairs");
require(factory.allPairsLength() == 2, "two pairs");
(uint256 rA, uint256 rB) = router.getReserves(address(tta), address(ttb));
require(rA == 500 ether && rB == 500 ether, "AB reserves");
(uint256 rT, uint256 rW) = router.getReserves(address(tta), address(wign));
require(rT == 500 ether && rW == 100 ether, "AW reserves");
require(IgneumPair(pairAB).balanceOf(alice) == 500 ether - 1000, "LP minus the locked minimum");
require(IgneumPair(pairAB).balanceOf(address(0xdead)) == 1000, "locked minimum");
require(wign.balanceOf(pairAW) == 100 ether, "WIGN held by the pair");
}
function test_swap_exact_tokens_for_tokens_keeps_k() public {
(address pairAB,) = _seed();
_drip(bob);
address[] memory path = new address[](2);
path[0] = address(tta);
path[1] = address(ttb);
uint256[] memory quoted = router.getAmountsOut(10 ether, path);
// 10 in at 0.3 percent on 500/500: 10*997*500 / (500*1000 + 10*997) tokens
require(quoted[1] == 9775084808910328058, "quote");
(uint112 r0b, uint112 r1b,) = IgneumPair(pairAB).getReserves();
vm.prank(bob);
uint256[] memory amounts = router.swapExactTokensForTokens(10 ether, quoted[1], path, bob, block.timestamp + 60);
require(amounts[1] == quoted[1], "swap matches the quote");
require(ttb.balanceOf(bob) == 1_000 ether + quoted[1], "bob received");
(uint112 r0a, uint112 r1a,) = IgneumPair(pairAB).getReserves();
require(uint256(r0a) * uint256(r1a) >= uint256(r0b) * uint256(r1b), "k did not fall");
}
function test_swap_ign_both_ways() public {
_seed();
address[] memory path = new address[](2);
path[0] = address(wign);
path[1] = address(tta);
uint256 before = tta.balanceOf(bob);
vm.prank(bob);
uint256[] memory amounts = router.swapExactIGNForTokens{value: 1 ether}(0, path, bob, block.timestamp + 60);
require(tta.balanceOf(bob) == before + amounts[1] && amounts[1] > 4.9 ether && amounts[1] < 5 ether, "IGN to TTA");
path[0] = address(tta);
path[1] = address(wign);
uint256 ignBefore = bob.balance;
vm.startPrank(bob);
tta.approve(address(router), type(uint256).max);
uint256[] memory back = router.swapExactTokensForIGN(amounts[1], 0, path, bob, block.timestamp + 60);
vm.stopPrank();
require(bob.balance == ignBefore + back[1] && back[1] < 1 ether && back[1] > 0.99 ether, "TTA to IGN");
}
function test_remove_liquidity_returns_both_tokens() public {
(address pairAB,) = _seed();
uint256 lp = IgneumPair(pairAB).balanceOf(alice);
vm.startPrank(alice);
IgneumPair(pairAB).approve(address(router), lp);
(uint256 a, uint256 b) = router.removeLiquidity(address(tta), address(ttb), lp, 0, 0, alice, block.timestamp + 60);
vm.stopPrank();
require(a == 500 ether - 1000 && b == 500 ether - 1000, "pro rata minus the locked share");
require(IgneumPair(pairAB).balanceOf(alice) == 0, "LP burned");
}
function test_expired_deadline_is_refused() public {
_seed();
address[] memory path = new address[](2);
path[0] = address(tta);
path[1] = address(ttb);
vm.prank(alice);
vm.expectRevert(bytes("Router: expired"));
router.swapExactTokensForTokens(1 ether, 0, path, alice, block.timestamp - 1);
}
}

23
contracts/dex/test/Vm.sol Normal file
View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// The Foundry cheatcodes this project uses, declared here so the tree needs no remote dependency.
interface Vm {
function startBroadcast(uint256 privateKey) external;
function stopBroadcast() external;
function envUint(string calldata name) external view returns (uint256);
function envAddress(string calldata name) external view returns (address);
function envOr(string calldata name, string calldata defaultValue) external view returns (string memory);
function toString(address value) external pure returns (string memory);
function toString(uint256 value) external pure returns (string memory);
function writeFile(string calldata path, string calldata data) external;
function deal(address who, uint256 newBalance) external;
function prank(address msgSender) external;
function startPrank(address msgSender) external;
function stopPrank() external;
function warp(uint256 newTimestamp) external;
function expectRevert(bytes calldata revertData) external;
function addr(uint256 privateKey) external pure returns (address);
}
address constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));

View file

@ -0,0 +1,24 @@
{
"network": "igneum-devnet-3",
"chain_id": 4463,
"chain_id_hex": "0x116f",
"note": "Devnet 3, test tokens, no value. The chain may reset; these addresses go with it.",
"rpc": "https://rpc.devnet.igneum.network",
"deployed_at": "2026-10-08T10:50:00Z",
"deployer": "0x07DD4DBca5c1a66755AF28BACCA1D901a2D209aA",
"source": "contracts/dex (Foundry, solc 0.8.28, evm_version paris, optimizer 200 runs)",
"contracts": {
"WIGN": { "address": "0x47447783D00e1760Eb815a34707b611D53f2A51e", "tx": "0x9fc48e0e3aa6ccdc41ff72381e1986d617722e22cd2b5f0f9f1e988eef60b2d6", "block": 26917 },
"TTA": { "address": "0x9ad4F0435f9172A89C6765eE8631CAB2Db3B5052", "tx": "0xa16b6a6501d2705b7c7dc4cf01f8b65b87270e5f1f90b4638b19f120b4830dc0", "block": 26919, "name": "Test Token A", "faucet": "drip() mints 1,000 TTA to the caller once an hour" },
"TTB": { "address": "0x996e3042089E80029348d7836472DB34C21841c1", "tx": "0x63eca8238d7f2c29e67c5aad33b0f1cb341d233f2ae0922621e4ba09d94eca61", "block": 26921, "name": "Test Token B", "faucet": "drip() mints 1,000 TTB to the caller once an hour" },
"IgneumFactory": { "address": "0x09FF42dbb20448ddB673fD34F973a7D93E7139A8", "tx": "0xd25223b5f1adcba415e51a0936c1ba3e9ae1dcbb1299ec5c4a91293e41ace3bb", "block": 26923 },
"IgneumRouter": { "address": "0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7", "tx": "0xabce2fcac81f87fdca02ac4528d4029ebbe1e6d49304dbe6427e1320f2e17b27", "block": 26924 }
},
"pairs": {
"TTA_WIGN": { "address": "0x5352Fc2723014Bb45A5b1767Df549DA755773A2B", "seeded": "500 TTA beside 100 IGN", "block": 26969 },
"TTB_WIGN": { "address": "0x93Db70744e200D647f783C3953dF35d7394EdBC5", "seeded": "500 TTB beside 100 IGN", "block": 26970 },
"TTA_TTB": { "address": "0xF35775db56dE4f1Cd8Ed7ea7CAb8D017d584a96F", "seeded": "500 TTA beside 500 TTB", "block": 26972 }
},
"fee": "0.3 percent per swap, kept in the pool",
"gas_note": "Devnet 3 charges the proving dimension inside the execution gas: a local EVM estimate runs out (drip() failed at 133,603 gas in block 26925; the node's eth_estimateGas said 685,513). Send with the node's own estimate (forge script --skip-simulation, or eth_estimateGas before every send)."
}

View file

@ -78,6 +78,7 @@ main{flex:1}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -112,6 +113,7 @@ main{flex:1}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -126,6 +126,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -160,6 +161,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -100,6 +100,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -134,6 +135,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -105,6 +105,7 @@ table{min-width:560px}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -139,6 +140,7 @@ table{min-width:560px}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -126,6 +126,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -160,6 +161,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -104,6 +104,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -138,6 +139,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -104,6 +104,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee" aria-current="page"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -138,6 +139,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee" aria-current="page"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -94,6 +94,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -128,6 +129,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -115,6 +115,7 @@ td.mono{font-family:var(--f-mono);font-size:12.5px;min-width:180px}td.iv{color:v
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -149,6 +150,7 @@ td.mono{font-family:var(--f-mono);font-size:12.5px;min-width:180px}td.iv{color:v
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -125,6 +125,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -159,6 +160,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -107,6 +107,7 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:14px;overflo
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet" aria-current="page"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -141,6 +142,7 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:14px;overflo
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet" aria-current="page"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -64,6 +64,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -98,6 +99,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -96,6 +96,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -130,6 +131,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -104,6 +104,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -138,6 +139,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -89,7 +89,7 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<div class="nav-groups" id="nav-groups" role="list">
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" data-active aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
</div>
<div class="nav-controls">
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
@ -108,6 +108,7 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -142,6 +143,7 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>
@ -154,7 +156,7 @@ blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);c
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/ledger" data-nav="ledger" aria-current="page"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>

View file

@ -161,6 +161,7 @@ body.all .pager{display:none}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -195,6 +196,7 @@ body.all .pager{display:none}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -245,6 +245,7 @@ details.tablebar summary{display:flex;align-items:center}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -279,6 +280,7 @@ details.tablebar summary{display:flex;align-items:center}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -106,6 +106,7 @@ ol{margin:0 0 14px;padding-left:22px}li{margin-bottom:6px}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask" aria-current="page"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -140,6 +141,7 @@ ol{margin:0 0 14px;padding-left:22px}li{margin-bottom:6px}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask" aria-current="page"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -101,6 +101,7 @@ pre b{color:var(--molten-text);font-weight:500}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -135,6 +136,7 @@ pre b{color:var(--molten-text);font-weight:500}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -105,6 +105,7 @@ table{min-width:560px}
<a href="/miners" data-nav="miners" aria-current="page"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -139,6 +140,7 @@ table{min-width:560px}
<a href="/miners" data-nav="miners" aria-current="page"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -28,6 +28,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -62,6 +63,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -105,6 +105,7 @@ table{min-width:560px}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -139,6 +140,7 @@ table{min-width:560px}
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -104,6 +104,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -138,6 +139,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -82,6 +82,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -116,6 +117,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

540
site/swap.html Normal file
View file

@ -0,0 +1,540 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Igneum swap on Devnet 3. Test tokens, no value</title>
<meta name="description" content="Swap wrapped IGN and two faucet test tokens on the Igneum zkEVM, Devnet 3. Every swap is a proven block; its proof status is read live. Test tokens, no value.">
<link rel="canonical" href="https://igneum.network/swap">
<meta name="theme-color" content="#0C0C0E">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Igneum">
<meta property="og:title" content="Igneum swap on Devnet 3">
<meta property="og:description" content="Swap test tokens on Igneum Devnet 3. Every swap is a proven block. Test tokens, no value.">
<meta property="og:url" content="https://igneum.network/swap">
<meta property="og:image" content="https://igneum.network/og.png?v=3">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Igneum swap on Devnet 3">
<meta name="twitter:description" content="Swap test tokens on Igneum Devnet 3. Every swap is a proven block. Test tokens, no value.">
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="/site.webmanifest">
<!-- head:start -->
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
<meta name="color-scheme" content="dark light">
<link rel="stylesheet" href="/site.css">
<style>
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
</style>
<script>
/* theme before first paint: ?theme=light|dark for one view (the capture tool), else the stored choice under igneum-theme, else the system setting; ?motion=off marks the page data-motion=off so every .reveal is drawn at once (a headless render never scrolls) */
(function(){var t=null;try{t=new URLSearchParams(location.search).get("theme");}catch(e){}if(t!=="light"&&t!=="dark"){try{t=localStorage.getItem("igneum-theme");}catch(e){t=null;}}if(t!=="light"&&t!=="dark")t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);try{if(new URLSearchParams(location.search).get("motion")==="off")document.documentElement.setAttribute("data-motion","off");}catch(e){}})();
</script>
<!-- head:end -->
<style>
/* page block; tokens, type and components are in /site.css */
main{flex:1;padding-bottom:100px}.card{background:var(--row);border:1px solid var(--line);border-radius:16px;padding:35px}.card h2{font-size:25px;margin-bottom:18px}.card h3{font-size:17px;margin:30px 0 8px}
.head{padding:65px 0 45px}.head p{color:var(--ink-2);max-width:67ch;font-size:18px;line-height:1.65}
.grid{display:grid;gap:24px;grid-template-columns:minmax(0,1fr)}
@media(min-width:900px){.grid{grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);gap:40px}}
form{display:flex;flex-direction:column;gap:12px;margin-top:16px}
label{display:block;font-size:13px;margin:16px 0 4px;color:var(--ink-2)}
input,select{background:var(--obsidian);border:1px solid var(--line-2);border-radius:7px;padding:14px 15px;color:var(--bone);width:100%;font:14px var(--f-mono)}
input:focus,select:focus{outline:2px solid var(--ember);outline-offset:2px;border-color:var(--ember)}
input[readonly]{color:var(--ash)}
.pair-row{display:grid;grid-template-columns:minmax(0,1fr) 110px;gap:10px}
.wallet-row{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin-bottom:6px}
.faucet-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px}
.btn#go{width:100%;margin-top:12px}.btn#flip{align-self:flex-start;padding:8px 14px;font-size:13px}
.btn[disabled]{opacity:.6;cursor:default;transform:none}
.status{min-height:28px;margin-top:4px;font-size:15px;color:var(--ink-2);overflow-wrap:anywhere}
.status[data-state="ok"]{color:var(--ok)}
.status[data-state="bad"]{color:var(--bad)}
.status code,td code,dd code{font-family:var(--f-mono);font-size:13px;background:var(--obsidian);padding:1px 6px;border-radius:6px}
p.small,.small{font-size:14px;color:var(--ash);line-height:1.55}
table.swaps,table.pools{width:100%;border-collapse:collapse;font-size:14px;margin-top:8px}
table.swaps th,table.pools th{text-align:left;color:var(--ash);font-weight:500;padding:8px 6px;border-bottom:1px solid var(--line)}
table.swaps td,table.pools td{padding:10px 6px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}
table.swaps td[data-state="bad"]{color:var(--bad)}
table.swaps tr.empty td{color:var(--ash)}
p.notice{margin-top:14px;padding:12px 14px;border-radius:10px;border:1px solid var(--line-2);font-size:13px;color:var(--molten);display:block}
dl{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;margin:22px 0 0;font-size:15px}
dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:13px;overflow-wrap:anywhere}
@media (prefers-reduced-motion:reduce){.btn:hover{transform:none}}
</style>
</head>
<body>
<!-- nav:start -->
<a class="skip" href="#main">Skip to content</a>
<nav class="nav" aria-label="Main" data-nav-version="5">
<div class="container nav-row">
<a href="/" class="logo" aria-label="Igneum home">
<svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
<span class="word">IGNEUM</span>
</a>
<span class="devnet" id="nav-devnet" title="igneum-devnet-3, chain id 4463. Coins have no value. The chain may reset."><span class="dot off" id="foot-dot"></span><span id="foot-state">devnet</span></span>
<div class="nav-groups" id="nav-groups" role="list">
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
</div>
<div class="nav-controls">
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
<a href="/download" class="btn primary small cta" data-nav="download">Download</a>
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Open the menu" aria-haspopup="true"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path class="bars" d="M4 6h16M4 12h16M4 18h16"/><path class="cross" d="m6 6 12 12M6 18 18 6"/></svg></button>
</div>
</div>
<div class="panels" id="nav-panels">
<div class="panel-wrap container">
<div class="nav-panel" id="nav-panel-mine" data-group="mine" role="region" aria-labelledby="nav-btn-mine" hidden>
<div class="panel-list">
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
<div class="panel-list">
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
</div>
</div>
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
<div class="panel-list">
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
</div>
</div>
</div>
<div class="nav-sheet" id="nav-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden>
<div class="sheet-scroll">
<div class="sheet-group"><div class="sheet-head">Mine</div>
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Learn</div>
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
</div>
<div class="sheet-foot">
<a href="/download" class="btn primary big" data-nav="download">Download</a>
<div class="sheet-social"><a href="https://discord.gg/igneum" target="_blank" rel="noopener">Discord</a><a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener">Source</a><a href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener">Reddit</a></div>
</div>
</div>
</div>
</nav>
<script>
(function(){
// the slim bar (site-ui-5, 7 Oct 2026): three group buttons open one panel each; the burger opens the sheet on phones.
// Keyboard: Enter or Space opens a group, Escape closes it and returns focus to its button, arrows move between the
// group buttons, Tab walks the open panel's links, a click outside closes. The theme toggle flips data-theme and stores it.
var n=document.querySelector('.nav');if(!n)return;
var btns=[].slice.call(n.querySelectorAll('.group-btn')),panels=[].slice.call(n.querySelectorAll('.nav-panel')),wrap=n.querySelector('.panels');
var burger=document.getElementById('nav-burger'),sheet=document.getElementById('nav-sheet');
function panelOf(g){return panels.filter(function(p){return p.getAttribute('data-group')===g;})[0];}
function closeAll(focusBack){var was=null;btns.forEach(function(b){if(b.getAttribute('aria-expanded')==='true')was=b;b.setAttribute('aria-expanded','false');});panels.forEach(function(p){p.hidden=true;});n.classList.remove('panel-open');if(focusBack&&was)was.focus();}
function open(g){closeAll(false);var b=btns.filter(function(x){return x.getAttribute('data-group')===g;})[0],p=panelOf(g);if(!b||!p)return;b.setAttribute('aria-expanded','true');p.hidden=false;n.classList.add('panel-open');anchor(b,p);}
// the panel sits under its button; a panel that would run past the container's right edge aligns its right edge to the button's right edge instead (never past the viewport)
function anchor(b,p){var pw=n.querySelector('.panel-wrap');if(!pw)return;var cr=pw.getBoundingClientRect(),br=b.getBoundingClientRect(),w=p.offsetWidth,left=br.left-cr.left;if(left+w>cr.width)left=(br.right-cr.left)-w;if(left<0)left=0;p.style.setProperty('--panel-left',Math.round(left)+'px');}
window.addEventListener('resize',function(){var o=btns.filter(function(x){return x.getAttribute('aria-expanded')==='true';})[0];if(o)anchor(o,panelOf(o.getAttribute('data-group')));}); btns.forEach(function(b,i){
b.addEventListener('click',function(){var on=b.getAttribute('aria-expanded')==='true';if(on)closeAll(false);else open(b.getAttribute('data-group'));});
b.addEventListener('keydown',function(e){
var k=e.key;
if(k==='ArrowRight'||k==='ArrowLeft'){e.preventDefault();var j=(i+(k==='ArrowRight'?1:btns.length-1))%btns.length;btns[j].focus();if(n.classList.contains('panel-open'))open(btns[j].getAttribute('data-group'));}
else if(k==='ArrowDown'){e.preventDefault();open(b.getAttribute('data-group'));var f=panelOf(b.getAttribute('data-group')).querySelector('a');if(f)f.focus();}
else if(k==='Escape'){closeAll(true);}
});
});
if(wrap)wrap.addEventListener('keydown',function(e){if(e.key==='Escape'){e.stopPropagation();closeAll(true);}});
document.addEventListener('click',function(e){if(!n.classList.contains('panel-open'))return;if(e.target.closest('.group-btn')||e.target.closest('.nav-panel'))return;closeAll(false);});
document.addEventListener('focusin',function(e){if(!n.classList.contains('panel-open'))return;if(n.contains(e.target))return;closeAll(false);});
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('panel-open'))closeAll(true);});
// the sheet on phones
function setSheet(o){if(!burger||!sheet)return;n.classList.toggle('open',o);sheet.hidden=!o;burger.setAttribute('aria-expanded',o?'true':'false');burger.setAttribute('aria-label',o?'Close the menu':'Open the menu');document.documentElement.classList.toggle('nav-locked',o);if(o){var f=sheet.querySelector('a');if(f)f.focus();}}
if(burger&&sheet){
burger.addEventListener('click',function(){setSheet(!n.classList.contains('open'));});
sheet.addEventListener('click',function(e){if(e.target.closest('a'))setSheet(false);});
sheet.addEventListener('keydown',function(e){
if(e.key==='Escape'){setSheet(false);burger.focus();return;}
if(e.key!=='Tab')return;var f=sheet.querySelectorAll('a,button');if(!f.length)return;var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();burger.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();burger.focus();}
});
burger.addEventListener('keydown',function(e){if(e.key==='Tab'&&!e.shiftKey&&n.classList.contains('open')){e.preventDefault();var f=sheet.querySelector('a');if(f)f.focus();}});
var mq=window.matchMedia('(min-width:861px)');if(mq.addEventListener)mq.addEventListener('change',function(){setSheet(false);});
}
// the theme toggle
var t=n.querySelector('[data-theme-toggle]');
function paintToggle(){var dark=document.documentElement.getAttribute('data-theme')!=='light';t.setAttribute('aria-label',dark?'Switch to light theme':'Switch to dark theme');t.innerHTML=dark?'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>':'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.7 13.3A8.5 8.5 0 0 1 10.7 3.3 8.5 8.5 0 1 0 20.7 13.3Z"/></svg>';}
if(t){t.addEventListener('click',function(){var v=document.documentElement.getAttribute('data-theme')==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',v);try{localStorage.setItem('igneum-theme',v);}catch(e){}document.dispatchEvent(new CustomEvent('igneum-theme'));});paintToggle();document.addEventListener('igneum-theme',paintToggle);}
})();
</script>
<!-- nav:end -->
<main id="main" class="wrap">
<div class="head">
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><span>Swap</span></div>
<div class="eyebrow"><span class="line"></span>Devnet 3 swap</div>
<h1>Swap test tokens <span class="accent">on Devnet 3.</span></h1>
<p>A constant-product exchange on the Igneum zkEVM: wrapped IGN and two faucet tokens in three pools, 0.3 percent to the pool. Every swap is a Devnet 3 block, and the block's proof status is read live below.</p>
<p class="notice">Devnet 3, test tokens, no value. The chain may reset and these pools go with it. Nothing here is an offer, a price or a market.</p>
</div>
<div class="grid">
<section class="card" aria-labelledby="swap-h">
<h2 id="swap-h">Swap</h2>
<div class="wallet-row">
<button class="btn" id="connect" type="button">Connect wallet</button>
<span class="status" id="wallet" role="status" aria-live="polite">No wallet connected. MetaMask or the Igneum Wallet, pointed at chain id 4463.</span>
</div>
<form id="swap-form" novalidate>
<label for="amount-in">You send</label>
<div class="pair-row">
<input id="amount-in" name="amount-in" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0" aria-describedby="status">
<select id="token-in" aria-label="Token to send"></select>
</div>
<button class="btn ghost" id="flip" type="button" aria-label="Swap direction">Flip</button>
<label for="amount-out">You receive (quoted)</label>
<div class="pair-row">
<input id="amount-out" name="amount-out" readonly placeholder="0.0">
<select id="token-out" aria-label="Token to receive"></select>
</div>
<p class="small" id="quote-line">Enter an amount for a quote. Slippage limit 1 percent, deadline 10 minutes.</p>
<button class="btn" id="go" type="submit" disabled>Swap</button>
<div class="status" id="status" role="status" aria-live="polite"></div>
</form>
<h3>Your swaps this session</h3>
<p class="small">Each row is a Devnet 3 block. Proof status comes from the public feed (/api/live): planned, proving, verified or paid per shard, and whether a finality checkpoint has locked the block.</p>
<table class="swaps" id="swaps" aria-label="Your swaps">
<thead><tr><th>Swap</th><th>Transaction</th><th>Block</th><th>Proof status</th></tr></thead>
<tbody id="swaps-body"><tr class="empty"><td colspan="4">No swaps yet this session.</td></tr></tbody>
</table>
</section>
<aside class="card" aria-labelledby="pools-h">
<h2 id="pools-h">Pools</h2>
<p class="small">Reserves read live from the Devnet 3 RPC. Test tokens come from each token's own faucet: one <code>drip()</code> of 1,000 an hour per address.</p>
<table class="pools" id="pools" aria-label="Pools">
<thead><tr><th>Pool</th><th>Reserves</th></tr></thead>
<tbody id="pools-body"><tr><td colspan="2">Reading the chain…</td></tr></tbody>
</table>
<div class="faucet-row">
<button class="btn ghost" id="drip-tta" type="button" disabled>Get 1,000 TTA</button>
<button class="btn ghost" id="drip-ttb" type="button" disabled>Get 1,000 TTB</button>
</div>
<div class="status" id="faucet-status" role="status" aria-live="polite"></div>
<h3>Add liquidity</h3>
<p class="small">Deposit both sides at the pool's ratio and receive IGN-LP tokens; remove them in the same shape through the router.</p>
<form id="lp-form" novalidate>
<div class="pair-row">
<select id="lp-pool" aria-label="Pool"></select>
<input id="lp-amount" inputmode="decimal" autocomplete="off" placeholder="amount of the first token">
</div>
<p class="small" id="lp-line"></p>
<button class="btn ghost" id="lp-go" type="submit" disabled>Add liquidity</button>
<div class="status" id="lp-status" role="status" aria-live="polite"></div>
</form>
<dl>
<dt>Chain</dt><dd>igneum-devnet-3, chain id 4463 (0x116f)</dd>
<dt>RPC</dt><dd><code>https://rpc.devnet.igneum.network</code></dd>
<dt>Router</dt><dd><code class="addr" data-addr="router"></code></dd>
<dt>Factory</dt><dd><code class="addr" data-addr="factory"></code></dd>
<dt>WIGN</dt><dd><code class="addr" data-addr="wign"></code></dd>
<dt>TTA</dt><dd><code class="addr" data-addr="tta"></code></dd>
<dt>TTB</dt><dd><code class="addr" data-addr="ttb"></code></dd>
<dt>Source</dt><dd>contracts/dex in the repository; addresses in docs/contracts/devnet-3.json</dd>
<dt>Wallet set-up</dt><dd><a href="/metamask">MetaMask: chain id, RPC, one click</a> or <a href="/wallet">the Igneum Wallet</a></dd>
<dt>IGN for gas</dt><dd><a href="/faucet">The faucet</a> or <a href="/miner#get">mine a block</a></dd>
</dl>
<p class="notice small">Devnet 3, test tokens, no value.</p>
</aside>
</div>
</main>
<!-- footer:start -->
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="/" class="logo" aria-label="Igneum home"><svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span><span class="stop">.</span></a>
<p>Mined by GPUs. Proven by fire.</p>
<div class="social" aria-label="Igneum elsewhere">
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" aria-label="Igneum source: the spec, the vectors and the issues"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8.6 17.6 3 12l5.6-5.6 1.4 1.4L5.8 12l4.2 4.2zm6.8 0-1.4-1.4L18.2 12 14 7.8l1.4-1.4L21 12z"></path></svg></a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
</div>
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
</div>
<div class="footer-column"><h4>Run</h4><div>
<a href="/download">Download Ember</a>
<a href="/miner">The miner</a>
<a href="/app">The app</a>
<a href="/wallet">The wallet</a>
<a href="/miners">GPU bench table</a>
<a href="/dev-fee">The dev fee</a>
<a href="/faucet">Testnet faucet</a>
<a href="/metamask">Add Igneum to MetaMask</a>
</div></div>
<div class="footer-column"><h4>Read</h4><div>
<a href="/litepaper">Litepaper</a>
<a href="/evidence">Evidence</a>
<a href="/bench">Engineering log</a>
<a href="/ledger">Ledger: every criticism</a>
<a href="/claims">What Igneum does not claim</a>
<a href="/randomx">Igneum vs RandomX</a>
<a href="/provenance">Built on the shoulders</a>
</div></div>
<div class="footer-column"><h4>Explore</h4><div>
<a href="/live">Live devnet</a>
<a href="/explorer">Explorer</a>
<a href="/journey">Journey</a>
<a href="/download">Downloads: devnet build</a>
<a href="https://discord.gg/igneum" target="_blank" rel="noopener">Community Discord</a>
<a href="mailto:hello@igneum.network">hello@igneum.network</a>
<a href="https://git.igneum.network/igneum-network/spec/issues" rel="noopener">An issue on the spec</a>
</div></div>
</div>
<div class="footer-bottom">
<p>Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. Experimental software. Nothing on this site is an offer to sell anything.<br>Report a flaw by email or by an issue on the spec. Nobody from Igneum will ask for your seed. © 2026 Igneum · not an offer to sell anything</p>
<div class="theme-group" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></div>
</div>
</div>
</footer>
<script>
(function(){
// theme control: stores light or dark under igneum-theme; system clears it and follows the OS. The head applied the value before paint.
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
var media=window.matchMedia('(prefers-color-scheme: light)');
function stored(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
function apply(){var c=stored();document.documentElement.setAttribute('data-theme',c==='system'?(media.matches?'light':'dark'):c);bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});document.dispatchEvent(new CustomEvent('igneum-theme'));}
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}apply();});});
if(media.addEventListener)media.addEventListener('change',function(){if(stored()==='system')apply();});
document.addEventListener('igneum-theme',function(){var c=stored();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});});
bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===stored()?'true':'false');});
// the network state word in the status bar: one read of the observer per page view
var fs=document.getElementById('foot-state'),fd=document.getElementById('foot-dot');if(fs&&fd){fetch('/api/live?window=30',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){var on=d&&d.ok&&d.state&&!d.state.stale;fs.textContent=on?'devnet live':'devnet, observer stale';fd.className='dot '+(on?'live':'off');}).catch(function(){fs.textContent='devnet';});}
})();
</script>
<!-- footer:end -->
<script>
(function(){
// Devnet 3, test tokens, no value. Addresses: docs/contracts/devnet-3.json. Reads go to the public RPC; sends go
// through the connected wallet (EIP-1193: MetaMask and any wallet that injects window.ethereum), which asks the node
// for the gas limit itself (Devnet 3 charges proving gas inside execution gas, so a local estimate runs out).
var RPC='https://rpc.devnet.igneum.network', CHAIN_ID=4463, CHAIN_HEX='0x116f';
var A={router:'0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7',factory:'0x09FF42dbb20448ddB673fD34F973a7D93E7139A8',wign:'0x47447783D00e1760Eb815a34707b611D53f2A51e',tta:'0x9ad4F0435f9172A89C6765eE8631CAB2Db3B5052',ttb:'0x996e3042089E80029348d7836472DB34C21841c1'};
var TOKENS=[{key:'ign',sym:'IGN',name:'IGN (the coin, wrapped on the way in)',addr:A.wign,native:true},{key:'tta',sym:'TTA',name:'Test Token A',addr:A.tta},{key:'ttb',sym:'TTB',name:'Test Token B',addr:A.ttb}];
var POOLS=[{name:'TTA / IGN',a:A.tta,b:A.wign,sa:'TTA',sb:'IGN'},{name:'TTB / IGN',a:A.ttb,b:A.wign,sa:'TTB',sb:'IGN'},{name:'TTA / TTB',a:A.tta,b:A.ttb,sa:'TTA',sb:'TTB'}];
// selectors, computed with cast sig
var S={getAmountsOut:"0xd06ca61f",swapExactTokensForTokens:"0x38ed1739",swapExactIGNForTokens:"0x5cb0bc93",swapExactTokensForIGN:"0x65462d52",approve:"0x095ea7b3",allowance:"0xdd62ed3e",balanceOf:"0x70a08231",drip:"0x9f678cca",dripWait:"0xac6f0034",getReserves:"0xd52bb6f4",addLiquidity:"0xe8e33700",addLiquidityIGN:"0x72efadf7"};
var $=function(id){return document.getElementById(id);};
var provider=null,account=null,swaps=[];
function pad(h){h=h.replace(/^0x/,'');return ('0'.repeat(64)+h).slice(-64);}
function encUint(v){return pad(BigInt(v).toString(16));}
function encAddr(a){return pad(a.toLowerCase());}
// ABI encode: types uint|address|address[]; dynamic arrays go after the head with an offset
function enc(sel,types,vals){var head=[],tail=[],headLen=types.length*32;for(var i=0;i<types.length;i++){var t=types[i],v=vals[i];if(t==='address[]'){var off=headLen+tail.join('').length/2;head.push(encUint(off));var body=encUint(v.length);for(var j=0;j<v.length;j++)body+=encAddr(v[j]);tail.push(body);}else if(t==='address')head.push(encAddr(v));else head.push(encUint(v));}return sel+head.join('')+tail.join('');}
function words(hex){hex=hex.replace(/^0x/,'');var out=[];for(var i=0;i+64<=hex.length;i+=64)out.push(BigInt('0x'+hex.slice(i,i+64)));return out;}
var rpcId=1;
function rpc(method,params){return fetch(RPC,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',id:rpcId++,method:method,params:params})}).then(function(r){return r.json();}).then(function(j){if(j.error)throw new Error(j.error.message||'rpc error');return j.result;});}
function call(to,data){return rpc('eth_call',[{to:to,data:data},'latest']);}
function fmt(wei,dp){wei=BigInt(wei);dp=dp===undefined?4:dp;var s=wei.toString().padStart(19,'0');var i=s.slice(0,-18),f=s.slice(-18,-18+dp);return i.replace(/\B(?=(\d{3})+(?!\d))/g,',')+(dp?'.'+f:'');}
function parse(str){str=String(str).trim();if(!/^\d*(\.\d{0,18})?$/.test(str)||str===''||str==='.')return null;var p=str.split('.');var f=(p[1]||'').padEnd(18,'0');return BigInt((p[0]||'0')+f);}
function short(h){return h.slice(0,10)+'…'+h.slice(-6);}
function say(el,text,state,html){el.setAttribute('data-state',state||'');if(html)el.innerHTML=html;else el.textContent=text;}
function tokenByKey(k){for(var i=0;i<TOKENS.length;i++)if(TOKENS[i].key===k)return TOKENS[i];return null;}
// ---- the wallet ----
function connect(){
var eth=window.ethereum;
if(!eth){say($('wallet'),'No wallet found in this browser. Install MetaMask, or use the Igneum Wallet (its page connection comes in a later build; until then watch an address below).','bad');return;}
eth.request({method:'eth_requestAccounts'}).then(function(acc){
account=acc[0];provider=eth;
return eth.request({method:'eth_chainId'});
}).then(function(cid){
if(parseInt(cid,16)!==CHAIN_ID){
return eth.request({method:'wallet_switchEthereumChain',params:[{chainId:CHAIN_HEX}]}).catch(function(e){
if(e&&(e.code===4902||/unrecognized|not added/i.test(String(e.message))))return eth.request({method:'wallet_addEthereumChain',params:[{chainId:CHAIN_HEX,chainName:'Igneum Devnet 3 (test tokens, no value)',nativeCurrency:{name:'IGN',symbol:'IGN',decimals:18},rpcUrls:[RPC],blockExplorerUrls:['https://igneum.network/explorer']}]});
throw e;
});
}
}).then(function(){
say($('wallet'),'',"ok",'Connected <code>'+short(account)+'</code> on chain id '+CHAIN_ID+'. Devnet 3, test tokens, no value.');
$('connect').textContent='Connected';$('go').disabled=false;$('lp-go').disabled=false;$('drip-tta').disabled=false;$('drip-ttb').disabled=false;
eth.on&&eth.on('accountsChanged',function(a){account=a[0]||null;if(!account){provider=null;say($('wallet'),'Wallet disconnected.','bad');}});
eth.on&&eth.on('chainChanged',function(){location.reload();});
quote();
}).catch(function(e){say($('wallet'),'The wallet refused: '+(e&&e.message?e.message:e),'bad');});
}
function send(tx){return provider.request({method:'eth_sendTransaction',params:[Object.assign({from:account},tx)]});}
function waitReceipt(hash,tries){return rpc('eth_getTransactionReceipt',[hash]).then(function(r){if(r)return r;if(tries<=0)throw new Error('no receipt yet');return new Promise(function(res){setTimeout(res,2000);}).then(function(){return waitReceipt(hash,tries-1);});});}
function ensureAllowance(token,amount){
return call(token,enc(S.allowance,['address','address'],[account,A.router])).then(function(r){
if(words(r)[0]>=amount)return null;
return send({to:token,data:enc(S.approve,['address','uint256'],[A.router,(1n<<256n)-1n])}).then(function(h){return waitReceipt(h,60);});
});
}
// ---- quotes and swaps ----
function path(tin,tout){return [tin.addr,tout.addr];}
var quoteTimer=null,lastQuote=null;
function quote(){
var tin=tokenByKey($('token-in').value),tout=tokenByKey($('token-out').value),amt=parse($('amount-in').value);
lastQuote=null;$('amount-out').value='';
if(!tin||!tout||tin.key===tout.key){say($('quote-line'),'Pick two different tokens.','');return;}
if(amt===null||amt===0n){say($('quote-line'),'Enter an amount for a quote. Slippage limit 1 percent, deadline 10 minutes.','');return;}
if(rpcBehind){say($('quote-line'),'The public RPC is still syncing; quotes come when it reaches the pools.','bad');return;}
call(A.router,enc(S.getAmountsOut,['uint256','address[]'],[amt,path(tin,tout)])).then(function(r){
var w=words(r);var out=w[w.length-1];lastQuote={tin:tin,tout:tout,amt:amt,out:out,min:out*99n/100n};
$('amount-out').value=fmt(out,6);
say($('quote-line'),'Quote: '+fmt(amt,6)+' '+tin.sym+' for '+fmt(out,6)+' '+tout.sym+' (at least '+fmt(lastQuote.min,6)+' after 1 percent slippage). 0.3 percent stays in the pool.','');
}).catch(function(e){say($('quote-line'),'No quote: '+(e.message||e),'bad');});
}
function doSwap(e){
e.preventDefault();
if(!provider||!account){say($('status'),'Connect a wallet first.','bad');return;}
if(!lastQuote){say($('status'),'Enter an amount and wait for the quote.','bad');return;}
var q=lastQuote,deadline=Math.floor(Date.now()/1000)+600,p=path(q.tin,q.tout),tx,label=fmt(q.amt,4)+' '+q.tin.sym+' to '+q.tout.sym;
$('go').disabled=true;say($('status'),'Confirm in the wallet…','');
var pre=q.tin.native?Promise.resolve():ensureAllowance(q.tin.addr,q.amt);
pre.then(function(){
if(q.tin.native)tx={to:A.router,value:'0x'+q.amt.toString(16),data:enc(S.swapExactIGNForTokens,['uint256','address[]','address','uint256'],[q.min,p,account,deadline])};
else if(q.tout.native)tx={to:A.router,data:enc(S.swapExactTokensForIGN,['uint256','uint256','address[]','address','uint256'],[q.amt,q.min,p,account,deadline])};
else tx={to:A.router,data:enc(S.swapExactTokensForTokens,['uint256','uint256','address[]','address','uint256'],[q.amt,q.min,p,account,deadline])};
return send(tx);
}).then(function(hash){
var row={label:label,hash:hash,number:null,status:'sent, waiting for a block'};swaps.unshift(row);render();
say($('status'),'',"ok",'Sent. Transaction <code>'+short(hash)+'</code>; its block appears in the table when the node has it.');
return waitReceipt(hash,90).then(function(r){row.number=parseInt(r.blockNumber,16);row.ok=r.status==='0x1';row.status=row.ok?'in block, proof status pending':'the execution failed';render();refreshProofs();refreshPools();});
}).catch(function(e){say($('status'),'Not sent: '+(e&&e.message?e.message:e),'bad');}).then(function(){$('go').disabled=false;lastQuote=null;$('amount-in').value='';$('amount-out').value='';});
}
// ---- the table and proof status from /api/live ----
function render(){
var tb=$('swaps-body');if(!swaps.length){tb.innerHTML='<tr class="empty"><td colspan="4">No swaps yet this session.</td></tr>';return;}
tb.innerHTML=swaps.map(function(s){var h=s.hash.replace(/[^0-9a-fx]/gi,'');return '<tr><td>'+s.label+'</td><td><a data-href="'+['','tx',h].join('/')+'"><code>'+short(h)+'</code></a></td><td>'+(s.number!==null?'<a data-href="'+['','block',s.number].join('/')+'">'+s.number.toLocaleString('en-GB')+'</a>':'pending')+'</td><td data-state="'+(s.ok===false?'bad':'')+'">'+s.status+'</td></tr>';}).join('');
tb.querySelectorAll('a[data-href]').forEach(function(a){a.setAttribute('href',a.getAttribute('data-href'));});
}
function proofText(b){
var sh=b.shards||[];var parts=[];
if(sh.length){var c={};sh.forEach(function(x){c[x.state]=(c[x.state]||0)+1;});var n=sh.length;if(c.paid===n)parts.push('proven and paid, '+n+' of '+n+' shards');else if((c.verified||0)+(c.paid||0)===n)parts.push('proven, '+n+' of '+n+' shards verified');else if(c.proving)parts.push('proving, '+((c.verified||0)+(c.paid||0))+' of '+n+' shards done');else parts.push('planned, '+n+' shard'+(n>1?'s':''));}
else parts.push('in the feed, proof not yet planned');
if(b.locked||b.final)parts.push('under a finality lock');
return parts.join('; ');
}
function refreshProofs(){
if(!swaps.some(function(s){return s.number!==null;}))return;
fetch('/api/live?window=1800').then(function(r){return r.json();}).then(function(j){
var byNumber={};(j.blocks||[]).forEach(function(b){if(b.number!==null&&b.number!==undefined)byNumber[b.number]=b;});
swaps.forEach(function(s){if(s.number===null||s.ok===false)return;var b=byNumber[s.number];s.status=b?proofText(b):'not yet observed on Devnet 3 (the feed keeps the last 30 minutes)';});
render();
}).catch(function(){});
}
setInterval(refreshProofs,10000);
// ---- pools and the faucets ----
var DEPLOY_BLOCK=26972, rpcBehind=false;
function refreshPools(){
rpc('eth_blockNumber',[]).then(function(n){
var h=parseInt(n,16);rpcBehind=h<DEPLOY_BLOCK;
if(rpcBehind){$('pools-body').innerHTML='<tr><td colspan="2">The public RPC is still syncing: block '+h.toLocaleString('en-GB')+' of at least '+DEPLOY_BLOCK.toLocaleString('en-GB')+'. Reserves, quotes and swaps appear when it reaches the pools.</td></tr>';return;}
return readPools();
}).catch(function(){$('pools-body').innerHTML='<tr><td colspan="2">The public RPC did not answer. Try again in a minute.</td></tr>';});
}
function readPools(){
Promise.all(POOLS.map(function(p){return call(A.router,enc(S.getReserves,['address','address'],[p.a,p.b])).then(function(r){var w=words(r);return fmt(w[0],2)+' '+p.sa+' / '+fmt(w[1],2)+' '+p.sb;}).catch(function(){return 'unreadable';});})).then(function(rows){
$('pools-body').innerHTML=POOLS.map(function(p,i){return '<tr><td>'+p.name+'</td><td>'+rows[i]+'</td></tr>';}).join('');
});
}
function drip(token,sym){
if(!provider||!account){say($('faucet-status'),'Connect a wallet first.','bad');return;}
call(token,enc(S.dripWait,['address'],[account])).then(function(r){var w=Number(words(r)[0]);if(w>0)throw new Error('this address can drip '+sym+' again in '+Math.ceil(w/60)+' minutes');return send({to:token,data:S.drip});})
.then(function(h){say($('faucet-status'),'',"ok",'Sent. 1,000 '+sym+' arrive with the block of <code>'+short(h)+'</code>.');return waitReceipt(h,90);}).then(function(){refreshPools();})
.catch(function(e){say($('faucet-status'),'Not sent: '+(e&&e.message?e.message:e),'bad');});
}
function lpLine(){
var p=POOLS[Number($('lp-pool').value)||0],amt=parse($('lp-amount').value);if(!p||amt===null||amt===0n){say($('lp-line'),'Enter the amount of '+ (p?p.sa:'the first token')+'; the other side follows the pool ratio.','');return;}
call(A.router,enc(S.getReserves,['address','address'],[p.a,p.b])).then(function(r){var w=words(r);var other=amt*w[1]/w[0];say($('lp-line'),fmt(amt,4)+' '+p.sa+' beside '+fmt(other,4)+' '+p.sb+' at the pool ratio.','');});
}
function addLp(e){
e.preventDefault();if(!provider||!account){say($('lp-status'),'Connect a wallet first.','bad');return;}
var p=POOLS[Number($('lp-pool').value)||0],amt=parse($('lp-amount').value);if(amt===null||amt===0n){say($('lp-status'),'Enter an amount.','bad');return;}
$('lp-go').disabled=true;say($('lp-status'),'Reading the pool…','');
call(A.router,enc(S.getReserves,['address','address'],[p.a,p.b])).then(function(r){
var w=words(r),other=amt*w[1]/w[0],deadline=Math.floor(Date.now()/1000)+600;
return ensureAllowance(p.a,amt).then(function(){
if(p.b===A.wign)return send({to:A.router,value:'0x'+other.toString(16),data:enc(S.addLiquidityIGN,['address','uint256','uint256','uint256','address','uint256'],[p.a,amt,amt*99n/100n,other*99n/100n,account,deadline])});
return ensureAllowance(p.b,other).then(function(){return send({to:A.router,data:enc(S.addLiquidity,['address','address','uint256','uint256','uint256','uint256','address','uint256'],[p.a,p.b,amt,other,amt*99n/100n,other*99n/100n,account,deadline])});});
});
}).then(function(h){say($('lp-status'),'',"ok",'Sent. Transaction <code>'+short(h)+'</code>.');return waitReceipt(h,90);}).then(function(){refreshPools();})
.catch(function(e){say($('lp-status'),'Not sent: '+(e&&e.message?e.message:e),'bad');}).then(function(){$('lp-go').disabled=false;});
}
// ---- wiring ----
function fillSelects(){
var tin=$('token-in'),tout=$('token-out');TOKENS.forEach(function(t){tin.add(new Option(t.sym,t.key));tout.add(new Option(t.sym,t.key));});tin.value='ign';tout.value='tta';
POOLS.forEach(function(p,i){$('lp-pool').add(new Option(p.name,String(i)));});
document.querySelectorAll('.addr').forEach(function(el){el.textContent=A[el.getAttribute('data-addr')];});
}
fillSelects();refreshPools();setInterval(refreshPools,30000);
$('connect').addEventListener('click',connect);
$('swap-form').addEventListener('submit',doSwap);
$('amount-in').addEventListener('input',function(){clearTimeout(quoteTimer);quoteTimer=setTimeout(quote,300);});
$('token-in').addEventListener('change',quote);$('token-out').addEventListener('change',quote);
$('flip').addEventListener('click',function(){var a=$('token-in').value;$('token-in').value=$('token-out').value;$('token-out').value=a;quote();});
$('drip-tta').addEventListener('click',function(){drip(A.tta,'TTA');});
$('drip-ttb').addEventListener('click',function(){drip(A.ttb,'TTB');});
$('lp-pool').addEventListener('change',lpLine);$('lp-amount').addEventListener('input',lpLine);
$('lp-form').addEventListener('submit',addLp);
if(window.ethereum&&window.ethereum.selectedAddress)connect();
})();
</script>
</body>
</html>

View file

@ -114,6 +114,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
</div>
@ -148,6 +149,7 @@
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
<a href="/faucet" data-nav="faucet"><b>Testnet faucet</b><span>Test coin for an address, once a day.</span></a>
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
</div>
<div class="sheet-group"><div class="sheet-head">Network</div>

View file

@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const GROUPS = {
mine: ['/miner', '/download', '/app', '/wallet', '/miners', '/dev-fee', '/faucet', '/metamask'],
mine: ['/miner', '/download', '/app', '/wallet', '/miners', '/dev-fee', '/faucet', '/swap', '/metamask'],
network: ['/live', '/explorer', '/journey', '/bench', '/evidence'],
learn: ['/litepaper', '/income', '/ledger', '/claims', '/randomx', '/provenance'],
};