Built on Bitcoin Layer 1 via OPNet

The OpFi DEX

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.

Get Started → View Contracts
4 contracts
AssemblyScript → WASM
0.3%
Swap fee (x·y = k)
18 dec
Token precision
35 files
Contracts + Frontend + Scripts
L1 BTC
No bridges, no wrapping

Architecture

Four on-chain contracts connected by cross-contract calls, fronted by a React + Vite dApp.

User / Wallet
👛
OP_WALLET
ML-DSA signing, no PSBT leakage
🖥️
React + Vite
SwapPanel · LiquidityPanel
Entry Point
🔀
Router
swapExactIn · swapExactOut · addLiquidity · removeLiquidity
🏭
Factory
Pool registry — getPool(tokenA, tokenB)
Core AMM
🌊
LiquidityPool
x·y=k · LP tokens (OP_20) · 0.3% fee
🪙
OpFiToken
OP_20 · transferFrom · approve
Settlement
Bitcoin L1
Tapscript calldata · OPNet VM · WASM execution

Smart Contracts

All contracts compile to WASM via AssemblyScript + @btc-vision/btc-runtime.

🪙

OpFiToken

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.

mint (to: address, amount: uint256) → bool
transfer (to: address, amount: uint256) → bool
approve (spender: address, amount: uint256)
transferFrom (from, to: address, amount: uint256)
🌊

LiquidityPool

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.

addLiquidity (tokenA, tokenB, amt0, amt1, to)
swap (amountIn, amountOutMin, tokenIn, to)
removeLiquidity (liquidity, min0, min1, to)
getReserves () → (reserve0, reserve1)
🏭

Factory

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.

registerPool (tokenA, tokenB, pool) → bool
getPool (tokenA, tokenB) → poolHash
poolCount () → uint256
🔀

Router

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.

swapExactIn (amtIn, amtOutMin, tokIn, tokOut, to)
swapExactOut (amtOut, amtInMax, tokIn, tokOut, to)
getAmountOut (amtIn, resIn, resOut) → amtOut
getAmountIn (amtOut, resIn, resOut) → amtIn

AMM Mathematics

Uniswap V2-compatible constant-product formula implemented with SafeMath u256.

Swap Output — getAmountOut

amountOut = (amountIn × 997 × reserveOut)
─────────────────────────────────
(reserveIn × 1000) + (amountIn × 997) // 0.3% fee deducted from input before pricing
1 Multiply amountIn × 997 to remove 0.3% fee from numerator
2 Denominator grows by reserveIn × 1000 so the pool stays over-collateralised
3 Result: newReserveIn × newReserveOut ≥ k always holds

First Liquidity — LP Minting

liquidity = √(amount0 × amount1) − 1000 // Newton's method integer sqrt on u256
1 First depositor: geometric mean of amounts minus MINIMUM_LIQUIDITY = 1000
2 1000 LP tokens permanently locked to Address.dead() to prevent 0-liquidity attacks
3 Subsequent depositors: min(amt0 × supply / res0, amt1 × supply / res1)

Project Structure

npm workspaces monorepo — contracts, frontend, and deployment scripts as separate packages.

contracts/ — AssemblyScript
src/
├─ token/
│ ├─ OpFiToken.tsAS
│ └─ index.ts
├─ pool/
│ ├─ LiquidityPool.tsAS
│ └─ index.ts
├─ factory/
│ ├─ Factory.tsAS
│ └─ index.ts
└─ router/
   ├─ Router.tsAS
   └─ index.ts
 
asconfig.json
package.json
frontend/ — React + Vite
src/
├─ config/
│ └─ contracts.tsTS
├─ services/
│ └─ ContractService.tsTS
├─ hooks/
│ ├─ useOpNet.tsTS
│ └─ useSwap.tsTS
├─ components/
│ ├─ WalletConnect.tsxTSX
│ ├─ SwapPanel.tsxTSX
│ └─ LiquidityPanel.tsxTSX
├─ App.tsxTSX
└─ main.tsx
 
scripts/
├─ deploy.tsTS
└─ addLiquidity.tsTS

Tech Stack

All OPNet-native packages — no EVM bridges, no wrapping, no custody.

⚙️

AssemblyScript

@btc-vision/assemblyscript fork with closure support

🦾

btc-runtime

OPNet contract runtime — OP_NET, OP20, storage, SafeMath

📦

opnet (npm)

getContract → simulate → sendTransaction

⚛️

React 19 + Vite 6

Frontend dApp with hooks-first architecture

@btc-vision/bitcoin

networks.opnetTestnet — never bitcoinjs-lib

🔐

@btc-vision/walletconnect

OP_WALLET integration, ML-DSA signing

🚀

@btc-vision/transaction

TransactionFactory — deployments only, never contract calls

📐

TypeScript Law 2026

Strict types · no any · #private · readonly · bigint for sats

OPNet Rules Applied

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

Quickstart

From clone to live DEX on OPNet Testnet.

1

Install dependencies

Each workspace installs its own packages. Start with contracts.

# Root workspace cd opfi && npm install # Contracts (AssemblyScript) cd contracts npm uninstall assemblyscript 2>/dev/null npx npm-check-updates -u && npm install --prefer-online
2

Build the contracts

Compiles all 4 contracts to WASM via asc.

npm run build # → build/OpFiToken.wasm # → build/LiquidityPool.wasm # → build/Factory.wasm # → build/Router.wasm
3

Deploy to OPNet Testnet

Set your seed phrase, run the deploy script. Addresses are printed to console.

cd ../scripts && npm install MNEMONIC="your twenty four word seed phrase …" \ npx tsx deploy.ts # Outputs: # { token: "opt1q…", factory: "opt1q…", … }
4

Paste addresses into frontend config

Edit frontend/src/config/contracts.ts — fill in the deployed addresses.

// frontend/src/config/contracts.ts [networks.opnetTestnet, { token: 'opt1q…your-token-address', factory: 'opt1q…your-factory-address', router: 'opt1q…your-router-address', pools: { 'token0:token1': 'opt1q…' }, }]
5

Add initial liquidity

Seeds the first pool so swaps are immediately possible.

MNEMONIC="…" \ POOL="opt1q…pool" \ TOKEN="opt1q…token" \ npx tsx addLiquidity.ts
6

Launch the frontend

Vite dev server starts on port 3000. Connect OP_WALLET and start swapping.

cd ../frontend && npm install npm run dev # → http://localhost:3000