A fully decentralized exchange on Bitcoin — constant-product AMM, OP_20 liquidity pools, and a swap router, all compiled to WebAssembly and settled on Bitcoin L1 via Tapscript-encoded calldata.
Four on-chain contracts connected by cross-contract calls, fronted by a React + Vite dApp.
All contracts compile to WASM via AssemblyScript + @btc-vision/btc-runtime.
src/token/OpFiToken.ts
Standard OP_20 fungible token with a deployer-restricted mint. 1 billion max supply at 18 decimals. Base for all pool pairs.
src/pool/LiquidityPool.ts
Constant-product AMM that is itself the LP token (extends OP_20). Implements Newton's-method integer √ for first-liquidity seeding. 0.3% fee baked into the swap formula.
src/factory/Factory.ts
Pool registry. Deployer registers each new pool after deployment. Pair keys are order-independent via a commutative djb2 hash of sorted token address strings.
src/router/Router.ts
User-facing entry point. Validates slippage, resolves pools via Factory,
orchestrates approve → transfer → swap via
Blockchain.call(). Includes
pure-math quote helpers with no gas cost.
Uniswap V2-compatible constant-product formula implemented with SafeMath u256.
amountIn × 997 to remove 0.3% fee from numerator
reserveIn × 1000 so the pool stays over-collateralised
newReserveIn × newReserveOut ≥ k always holds
MINIMUM_LIQUIDITY = 1000
Address.dead() to prevent 0-liquidity attacks
min(amt0 × supply / res0, amt1 × supply / res1)
npm workspaces monorepo — contracts, frontend, and deployment scripts as separate packages.
All OPNet-native packages — no EVM bridges, no wrapping, no custody.
@btc-vision/assemblyscript fork with closure support
OPNet contract runtime — OP_NET, OP20, storage, SafeMath
getContract → simulate → sendTransaction
Frontend dApp with hooks-first architecture
networks.opnetTestnet — never bitcoinjs-lib
OP_WALLET integration, ML-DSA signing
TransactionFactory — deployments only, never contract calls
Strict types · no any · #private · readonly · bigint for sats
Every rule from the OPNet development guidelines is enforced across all files.
| Rule | Status | Applied Where |
|---|---|---|
| ML-DSA only (no ECDSA) | ✓ | All signature verification; scripts use wallet.mldsaKeypair |
| signer: null on frontend | ✓ | Every sendTransaction() in useSwap.ts and LiquidityPanel.tsx |
| signer: keypair on backend | ✓ | deploy.ts and addLiquidity.ts use both keypair + mldsaKeypair |
| No raw PSBT | ✓ | Zero occurrences of new Psbt() anywhere |
| TransactionFactory = deploy only | ✓ | scripts/deploy.ts only; contract calls use getContract → simulate |
| networks.opnetTestnet | ✓ | All network references; never networks.testnet |
| @method() with all params | ✓ | Every method in all 4 contracts declares full ABI params |
| onDeployment for storage init | ✓ | All contracts; constructors only call super() + declare pointers |
| SafeMath everywhere | ✓ | All arithmetic in LiquidityPool, Router, Factory |
| No bare Map<Address, T> | ✓ | StoredMapU256 in Factory, StoredString for address fields |
| Always simulate first | ✓ | useSwap.ts: simulateSwap() must succeed before executeSwap() |
| No import for transform globals | ✓ | @method, @returns, @emit, ABIDataTypes never imported |
From clone to live DEX on OPNet Testnet.
Each workspace installs its own packages. Start with contracts.
Compiles all 4 contracts to WASM via asc.
Set your seed phrase, run the deploy script. Addresses are printed to console.
Edit frontend/src/config/contracts.ts — fill in the deployed addresses.
Seeds the first pool so swaps are immediately possible.
Vite dev server starts on port 3000. Connect OP_WALLET and start swapping.