
QuickSwap V3 DEX
- 9 installs
- 12 repo stars
- Updated August 3, 2026
- okx/plugin-store
Helps with ai & agent building tasks during AI-assisted development.
About
QuickSwap V3 DEX is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- QuickSwap V3 DEX
- AI & Agent Building
- AI-coding skill
QuickSwap V3 DEX by the numbers
- 9 all-time installs (skills.sh)
- Ranked #12,133 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/okx/plugin-store --skill quickswap-v3-dexAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 3, 2026 |
| Repository | okx/plugin-store ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Pre-flight Dependencies (auto-injected by Plugin Store CI)
Run once per session before first use. These checks ensure required tools are installed.
Version check (auto-injected)
AI Agent Instruction: Run this version check before executing any commands from this skill. The check is a read-only fetch fromraw.githubusercontent.com— it does not install anything. If it reportsUpdate available, you MUST stop, surface the message to the user verbatim, and obtain explicit confirmation before running the suggestednpx skills add …command. Do not run the update silently. After a confirmed update completes, re-read this SKILL.md before proceeding.
# Check for skill updates (1-hour cache).
# Network scope: this block calls raw.githubusercontent.com for the version probe only.
# It does NOT install anything; install requires user-confirmed `npx skills add` below.
UPDATE_CACHE="$HOME/.plugin-store/update-cache/quickswap-plugin"
CACHE_MAX=3600
LOCAL_VER="0.1.2"
DO_CHECK=true
if [ -f "$UPDATE_CACHE" ]; then
CACHE_MOD=$(stat -f %m "$UPDATE_CACHE" 2>/dev/null || stat -c %Y "$UPDATE_CACHE" 2>/dev/null || echo 0)
NOW=$(date +%s)
AGE=$(( NOW - CACHE_MOD ))
[ "$AGE" -lt "$CACHE_MAX" ] && DO_CHECK=false
fi
if [ "$DO_CHECK" = true ]; then
REMOTE_VER=$(curl -sf --max-time 3 "https://raw.githubusercontent.com/okx/plugin-store/main/skills/quickswap-plugin/plugin.yaml" | grep '^version' | head -1 | tr -d '"' | awk '{print $2}')
if [ -n "$REMOTE_VER" ]; then
mkdir -p "$HOME/.plugin-store/update-cache"
echo "$REMOTE_VER" > "$UPDATE_CACHE"
fi
fi
REMOTE_VER=$(cat "$UPDATE_CACHE" 2>/dev/null || echo "$LOCAL_VER")
if [ "$REMOTE_VER" != "$LOCAL_VER" ]; then
echo "Update available: quickswap-plugin v$LOCAL_VER -> v$REMOTE_VER."
echo "ACTION REQUIRED: ask the user to confirm before running:"
echo " npx skills add okx/plugin-store --skill quickswap-plugin --global"
echo "(This contacts the npm registry and github.com/okx/plugin-store and overwrites this skill. Do NOT auto-run.)"
fiInstall onchainos CLI + Skills (auto-injected)
# 1. Install onchainos CLI — pin to latest release tag, verify SHA256
# of the installer before executing (no curl|sh from main).
if ! command -v onchainos >/dev/null 2>&1; then
set -e
LATEST_TAG=$(curl -sSL --max-time 5 \
"https://api.github.com/repos/okx/onchainos-skills/releases/latest" \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
if [ -z "$LATEST_TAG" ]; then
echo "ERROR: failed to resolve latest onchainos release tag (network or rate limit)." >&2
echo " Manual install: https://github.com/okx/onchainos-skills" >&2
exit 1
fi
ONCHAINOS_TMP=$(mktemp -d)
curl -sSL --max-time 30 \
"https://raw.githubusercontent.com/okx/onchainos-skills/${LATEST_TAG}/install.sh" \
-o "$ONCHAINOS_TMP/install.sh"
curl -sSL --max-time 30 \
"https://github.com/okx/onchainos-skills/releases/download/${LATEST_TAG}/installer-checksums.txt" \
-o "$ONCHAINOS_TMP/installer-checksums.txt"
EXPECTED=$(awk '$2 ~ /install\.sh$/ {print $1; exit}' "$ONCHAINOS_TMP/installer-checksums.txt")
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$ONCHAINOS_TMP/install.sh" | awk '{print $1}')
else
ACTUAL=$(shasum -a 256 "$ONCHAINOS_TMP/install.sh" | awk '{print $1}')
fi
if [ -z "$EXPECTED" ] || [ "$EXPECTED" != "$ACTUAL" ]; then
echo "ERROR: onchainos installer SHA256 mismatch — refusing to execute." >&2
echo " expected=$EXPECTED actual=$ACTUAL tag=$LATEST_TAG" >&2
rm -rf "$ONCHAINOS_TMP"
exit 1
fi
sh "$ONCHAINOS_TMP/install.sh"
rm -rf "$ONCHAINOS_TMP"
set +e
fi
# 2. Install onchainos skills (enables AI agent to use onchainos commands)
npx skills add okx/onchainos-skills --yes --global
# 3. Install plugin-store skills (enables plugin discovery and management)
npx skills add okx/plugin-store --skill plugin-store --yes --global---
QuickSwap V3 DEX
QuickSwap V3 is the leading concentrated liquidity DEX on Polygon, built on the Algebra Protocol (not Uniswap V3). Key difference: no fee tier parameter — fees are dynamic per pool.
Chain: Polygon (137) Contracts:
- SwapRouter:
0xf5b509bb0909a69b1c207e495f687a596c168e12 - Quoter:
0xa15F0D7377B2A0C0c10db057f641beD21028FC89 - Factory:
0x411b0fAcC3489691f28ad58c47006AF5E3Ab3A28
---
Pre-flight Dependencies
Before using this plugin, verify both dependencies are working:
# 1. Verify plugin binary
quickswap-plugin --version
# Expected: quickswap-plugin 0.1.2
# 2. Verify onchainos wallet is configured for Polygon
onchainos wallet addresses --chain 137
# Expected: JSON with at least one addressIf onchainos wallet addresses returns no addresses, configure a Polygon wallet first before proceeding.
---
Data Trust Boundary
| Source | Used For | Trust Level |
|---|---|---|
Polygon RPC (polygon.publicnode.com) | Token balances, decimals, allowances, quotes | On-chain — authoritative |
QuickSwap subgraph (api.thegraph.com) | Pool TVL and volume rankings | Off-chain indexer — informational only |
| onchainos CLI | Token resolution, tx broadcast | Local CLI — trusted |
Rule: Never use subgraph data for trade execution amounts. All swap parameters (amountIn, amountOut, slippage) are derived from on-chain Quoter calls only.
⚠️ Partial-state risk: swap --confirm executes up to 3 sequential transactions (wrap → approve → swap). If a transaction confirms but the binary times out waiting for the next (60-second poll), the swap halts mid-sequence. In that case: check your WMATIC/token balance to determine what was completed, then re-run the command — the approve step is idempotent (skipped if allowance is sufficient). Do not resubmit without checking state first.---
Proactive Onboarding
When a user signals they are new or just installed this plugin — e.g. "I just installed quickswap-plugin", "how do I swap on Polygon", "what can I do with this" — do not wait for them to ask specific questions. Proactively walk them through the Quickstart in order, one step at a time, waiting for confirmation before proceeding to the next:
1. Check wallet — run onchainos wallet addresses --chain 137. If no address, direct them to connect via onchainos wallet login. Do not proceed to write operations until a wallet is confirmed. 2. Check balance — run onchainos wallet balance --chain 137. The user needs POL/MATIC for gas (~0.01 POL) plus the token they want to swap. If balance is zero, explain how to bridge via a CEX or cross-chain bridge. 3. Token address tip — explain that QuickSwap has two USDC tokens: USDC.e (0x2791Bca1...4174, bridged, more liquid) and native USDC (0x3c499c...3359, CCTP-bridged). Use the full address to be unambiguous. 4. Get a quote first — run quickswap-plugin quote --token-in <address> --token-out <address> --amount <amount> so the user sees the expected output before committing. 5. Preview the swap — run quickswap-plugin swap without --confirm so the user sees all steps (approve, swap) and calldata. 6. Execute — once they confirm, re-run with --confirm.
Do not dump all steps at once. Guide conversationally — confirm each step before moving on.
---
Quickstart
New to QuickSwap V3? Follow these steps to go from zero to your first swap on Polygon.
Step 1 — Connect wallet
onchainos wallet login your@email.com
onchainos wallet addresses --chain 137Step 2 — Check balances
onchainos wallet balance --chain 137You need POL/MATIC for gas (~0.01 POL per swap). If balance is zero, bridge from a CEX or use a cross-chain bridge.
Step 3 — Browse top pools
quickswap-plugin pools --limit 10This shows the most liquid QuickSwap V3 pairs. When the subgraph is available it includes TVL and volume; otherwise shows top known pairs.
Step 4 — Get a quote
Use MATIC/POL as shorthand for native MATIC, or pass token addresses directly. For USDC, note that Polygon has two versions — use the full address:
# Quote using symbols (MATIC → native USDC)
quickswap-plugin quote --token-in MATIC --token-out USDC --amount 10
# Quote using addresses (USDC.e → WETH, more liquid pair)
quickswap-plugin quote \
--token-in 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 \
--token-out 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 \
--amount 5Expected output (USDC.e → WETH example):
{
"ok": true,
"tokenIn": "0x2791...4174",
"tokenOut": "0x7ceb...f619",
"amountIn": "5.000000",
"amountOut": "0.002187",
"price": "0.000437",
"chain": "Polygon"
}Step 5 — Preview a swap (safe, no broadcast)
# Preview (safe — no tx sent, no gas spent)
quickswap-plugin swap \
--token-in 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 \
--token-out 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 \
--amount 1Output shows "preview": true and all planned steps (approve + swap calldata) without broadcasting.
Step 6 — Execute the swap
# Execute (add --confirm):
quickswap-plugin swap \
--token-in 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 \
--token-out 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 \
--amount 1 \
--confirmExpected output: "ok": true, "explorerLink": "https://polygonscan.com/tx/0x...". The plugin approves the SwapRouter and waits for approval to mine before submitting the swap.
MATIC auto-wrap: If--token-in MATICor--token-in POL, the plugin automatically wraps native MATIC → WMATIC first (3 transactions total: wrap + approve + swap).
Step 7 — Verify on PolygonScan
Open the explorerLink from the output to verify the transaction. Gas used is typically 250,000–350,000 gas (~0.03–0.05 POL at standard gas prices).
---
Commands
quickswap-plugin swap
Swap tokens on QuickSwap V3 via the Algebra CLMM SwapRouter.
Flags:
| Flag | Required | Default | Description |
|---|---|---|---|
--token-in | Yes | — | Input token: symbol (MATIC, USDC, WETH) or address (0x...) |
--token-out | Yes | — | Output token: symbol or address |
--amount | Yes | — | Amount of tokenIn (human-readable, e.g. 10.5) |
--slippage | No | 0.5 | Max slippage in percent (0–50) |
--from | No | wallet default | Override sender address |
--confirm | No | false | Broadcast on-chain (omit for dry-run) |
Examples:
# Dry-run preview (default)
quickswap-plugin swap --token-in MATIC --token-out USDC --amount 10
# Execute with custom slippage
quickswap-plugin swap --token-in USDC --token-out WETH --amount 100 --slippage 1.0 --confirm
# Swap using token addresses
quickswap-plugin swap \
--token-in 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 \
--token-out 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 \
--amount 50 --confirmMATIC auto-wrap: If --token-in is MATIC or POL, the plugin will automatically wrap native MATIC → WMATIC before swapping. This adds one extra transaction.
---
quickswap-plugin quote
Get a price quote from the on-chain Quoter without executing a transaction. No wallet required.
Flags:
| Flag | Required | Description |
|---|---|---|
--token-in | Yes | Input token symbol or address |
--token-out | Yes | Output token symbol or address |
--amount | Yes | Amount of tokenIn |
Example:
quickswap-plugin quote --token-in MATIC --token-out USDC --amount 10
quickswap-plugin quote --token-in WETH --token-out USDT --amount 0.1---
quickswap-plugin pools
List top QuickSwap V3 pools ordered by TVL. Data sourced from the QuickSwap subgraph; falls back to hardcoded top pools if the subgraph is unavailable.
Flags:
| Flag | Default | Description |
|---|---|---|
--limit | 10 | Number of pools to return (max 20) |
Example:
quickswap-plugin pools
quickswap-plugin pools --limit 5---
Execution Mode Reference
| Command | Requires --confirm | On-chain Effect | Notes |
|---|---|---|---|
quote | No | None | Pure read — calls Quoter contract via RPC |
pools | No | None | Pure read — queries subgraph |
swap (no flag) | No | None | Dry-run preview only |
swap --confirm | Yes | Up to 3 txs | Wrap (if MATIC) + Approve + Swap |
---
Token Reference (Polygon)
| Symbol | Address |
|---|---|
| WMATIC | 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270 |
| USDC.e | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 |
| USDT | 0xc2132D05D31c914a87C6611C10748AEb04B58e8F |
| WETH | 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 |
| QUICK | 0x831753DD7087CaC61aB5644b308642cc1c33Dc13 |
---
Install
LOCAL_VER="0.1.2"
curl -L "https://github.com/skylavis-sky/plugin-store/releases/download/quickswap-plugin-v${LOCAL_VER}/quickswap-plugin-linux-x86_64" \
-o ~/.local/bin/quickswap-plugin && chmod +x ~/.local/bin/quickswap-plugin
# Verify
quickswap-plugin --versionFor macOS (Apple Silicon):
LOCAL_VER="0.1.2"
curl -L "https://github.com/skylavis-sky/plugin-store/releases/download/quickswap-plugin-v${LOCAL_VER}/quickswap-plugin-macos-arm64" \
-o ~/.local/bin/quickswap-plugin && chmod +x ~/.local/bin/quickswap-plugin{
"schema_version": 1,
"name": "quickswap-plugin",
"version": "0.1.2",
"description": "Swap tokens and provide concentrated liquidity on QuickSwap V3 CLMM on Polygon",
"binary": "quickswap"
}
[package]
name = "quickswap-plugin"
version = "0.1.2"
edition = "2021"
[[bin]]
name = "quickswap-plugin"
path = "src/main.rs"
[dependencies]
clap = { version = "4", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
alloy-primitives = "0.8"
alloy-sol-types = "0.8"
hex = "0.4"
MIT License
Copyright (c) 2026 skylavis-sky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
schema_version: 1
name: quickswap-plugin
version: "0.1.2"
description: "Swap tokens on QuickSwap V3 (Algebra CLMM) on Polygon — with price quotes and pool discovery"
author:
name: skylavis-sky
github: skylavis-sky
category: dapp
tags:
- quickswap
- polygon
- clmm
- swap
- defi
license: MIT
components:
skill:
dir: .
build:
lang: rust
binary_name: quickswap-plugin
api_calls:
- "https://polygon.publicnode.com"
- "https://api.thegraph.com"
- "https://polygonscan.com"
/// Manual ABI encoding helpers.
/// All addresses are left-padded to 32 bytes.
/// All uint256/uint128 values are left-padded to 32 bytes.
fn pad_address(addr: &str) -> [u8; 32] {
let addr = addr.trim_start_matches("0x");
let bytes = hex::decode(addr).unwrap_or_default();
let mut out = [0u8; 32];
let start = 32 - bytes.len().min(20);
out[start..].copy_from_slice(&bytes[bytes.len().saturating_sub(20)..]);
out
}
fn pad_u256(val: u128) -> [u8; 32] {
let mut out = [0u8; 32];
let bytes = val.to_be_bytes(); // 16 bytes
out[16..].copy_from_slice(&bytes);
out
}
fn pad_u160(val: u128) -> [u8; 32] {
// u160 fits in u128 for our use case (we only use 0)
pad_u256(val)
}
/// encode approve(address spender, uint256 amount)
/// selector: 0x095ea7b3
pub fn encode_erc20_approve(spender: &str, amount: u128) -> String {
let mut data = Vec::with_capacity(4 + 64);
data.extend_from_slice(&[0x09, 0x5e, 0xa7, 0xb3]);
data.extend_from_slice(&pad_address(spender));
data.extend_from_slice(&pad_u256(amount));
format!("0x{}", hex::encode(data))
}
/// encode allowance(address owner, address spender)
/// selector: 0xdd62ed3e
pub fn encode_allowance(owner: &str, spender: &str) -> String {
let mut data = Vec::with_capacity(4 + 64);
data.extend_from_slice(&[0xdd, 0x62, 0xed, 0x3e]);
data.extend_from_slice(&pad_address(owner));
data.extend_from_slice(&pad_address(spender));
format!("0x{}", hex::encode(data))
}
/// encode balanceOf(address account)
/// selector: 0x70a08231
pub fn encode_balance_of(account: &str) -> String {
let mut data = Vec::with_capacity(4 + 32);
data.extend_from_slice(&[0x70, 0xa0, 0x82, 0x31]);
data.extend_from_slice(&pad_address(account));
format!("0x{}", hex::encode(data))
}
/// encode decimals()
/// selector: 0x313ce567
pub fn encode_decimals() -> String {
"0x313ce567".to_string()
}
/// encode symbol()
/// selector: 0x95d89b41
pub fn encode_symbol() -> String {
"0x95d89b41".to_string()
}
/// encode exactInputSingle((address tokenIn, address tokenOut, address recipient,
/// uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 limitSqrtPrice))
/// selector: 0xbc651188
/// Algebra struct: tokenIn, tokenOut, recipient, deadline, amountIn, amountOutMinimum, limitSqrtPrice
pub fn encode_exact_input_single(
token_in: &str,
token_out: &str,
recipient: &str,
deadline: u64,
amount_in: u128,
amount_out_min: u128,
) -> String {
let mut data = Vec::with_capacity(4 + 7 * 32);
data.extend_from_slice(&[0xbc, 0x65, 0x11, 0x88]);
data.extend_from_slice(&pad_address(token_in));
data.extend_from_slice(&pad_address(token_out));
data.extend_from_slice(&pad_address(recipient));
data.extend_from_slice(&pad_u256(deadline as u128));
data.extend_from_slice(&pad_u256(amount_in));
data.extend_from_slice(&pad_u256(amount_out_min));
data.extend_from_slice(&pad_u160(0)); // limitSqrtPrice = 0 (no price limit)
format!("0x{}", hex::encode(data))
}
/// encode quoteExactInputSingle(address tokenIn, address tokenOut, uint256 amountIn, uint160 limitSqrtPrice)
/// selector: 0x2d9ebd1d
pub fn encode_quote_exact_input_single(
token_in: &str,
token_out: &str,
amount_in: u128,
) -> String {
let mut data = Vec::with_capacity(4 + 4 * 32);
data.extend_from_slice(&[0x2d, 0x9e, 0xbd, 0x1d]);
data.extend_from_slice(&pad_address(token_in));
data.extend_from_slice(&pad_address(token_out));
data.extend_from_slice(&pad_u256(amount_in));
data.extend_from_slice(&pad_u160(0)); // limitSqrtPrice = 0
format!("0x{}", hex::encode(data))
}
/// encode WMATIC deposit() — no params, selector: 0xd0e30db0
pub fn encode_wmatic_deposit() -> String {
"0xd0e30db0".to_string()
}
/// Decode a hex result (0x-prefixed) as u128 from the first 32 bytes
pub fn decode_u128(hex_str: &str) -> anyhow::Result<u128> {
let s = hex_str.trim_start_matches("0x");
if s.is_empty() || s == "0" {
return Ok(0);
}
// Take the last 32 bytes (64 hex chars) — right-aligned
let padded = format!("{:0>64}", s);
let last32 = &padded[padded.len().saturating_sub(64)..];
let bytes = hex::decode(last32)
.map_err(|e| anyhow::anyhow!("hex decode error: {}", e))?;
// Read last 16 bytes as u128
let slice = &bytes[bytes.len().saturating_sub(16)..];
let mut arr = [0u8; 16];
let copy_start = 16 - slice.len();
arr[copy_start..].copy_from_slice(slice);
Ok(u128::from_be_bytes(arr))
}
/// Decode a hex result as u8 (for decimals)
pub fn decode_u8(hex_str: &str) -> anyhow::Result<u8> {
let val = decode_u128(hex_str)?;
Ok(val as u8)
}
/// Decode a hex result as an ABI-encoded string (for symbol)
pub fn decode_string(hex_str: &str) -> anyhow::Result<String> {
let s = hex_str.trim_start_matches("0x");
if s.len() < 128 {
// Fallback: try direct ASCII decode of last bytes
if let Ok(bytes) = hex::decode(s) {
let trimmed: Vec<u8> = bytes.into_iter().filter(|&b| b >= 0x20 && b <= 0x7e).collect();
if !trimmed.is_empty() {
return Ok(String::from_utf8_lossy(&trimmed).to_string());
}
}
return Ok("UNKNOWN".to_string());
}
// ABI encoded string: offset (32 bytes) + length (32 bytes) + data
let bytes = hex::decode(s).map_err(|e| anyhow::anyhow!("hex decode: {}", e))?;
if bytes.len() < 64 {
return Ok("UNKNOWN".to_string());
}
// length is at offset 32
let len_bytes = &bytes[32..48];
let mut len_arr = [0u8; 16];
len_arr.copy_from_slice(len_bytes);
let len = u128::from_be_bytes(len_arr) as usize;
if bytes.len() < 64 + len {
return Ok("UNKNOWN".to_string());
}
let data = &bytes[64..64 + len];
Ok(String::from_utf8_lossy(data).to_string())
}
pub mod swap;
pub mod quote;
pub mod pools;
use reqwest::Client;
use serde_json::Value;
const SUBGRAPH_URL: &str =
"https://api.thegraph.com/subgraphs/name/sameepsi/quickswap-v3";
const GQL_QUERY: &str = r#"{
"query": "{ pools(first: 20, orderBy: totalValueLockedUSD, orderDirection: desc) { id token0 { symbol } token1 { symbol } totalValueLockedUSD volumeUSD feesUSD } }"
}"#;
/// Known top QuickSwap V3 pools (fallback when subgraph is unavailable)
fn fallback_pools() -> Vec<Value> {
vec![
serde_json::json!({
"pair": "WMATIC/USDC.e",
"token0": "WMATIC",
"token1": "USDC.e",
"source": "hardcoded"
}),
serde_json::json!({
"pair": "WETH/USDC.e",
"token0": "WETH",
"token1": "USDC.e",
"source": "hardcoded"
}),
serde_json::json!({
"pair": "WBTC/WETH",
"token0": "WBTC",
"token1": "WETH",
"source": "hardcoded"
}),
serde_json::json!({
"pair": "USDC.e/USDT",
"token0": "USDC.e",
"token1": "USDT",
"source": "hardcoded"
}),
serde_json::json!({
"pair": "QUICK/WMATIC",
"token0": "QUICK",
"token1": "WMATIC",
"source": "hardcoded"
}),
]
}
pub async fn run(limit: usize) -> anyhow::Result<Value> {
let client = Client::new();
let response = client
.post(SUBGRAPH_URL)
.header("Content-Type", "application/json")
.body(GQL_QUERY)
.send()
.await;
match response {
Ok(resp) => {
let json: Value = match resp.json().await {
Ok(v) => v,
Err(_) => {
return Ok(subgraph_unavailable_response(limit));
}
};
if let Some(errors) = json.get("errors") {
return Ok(serde_json::json!({
"ok": true,
"note": "subgraph returned errors",
"errors": errors,
"fallbackPools": &fallback_pools()[..fallback_pools().len().min(limit)]
}));
}
let pools_raw = json
.get("data")
.and_then(|d| d.get("pools"))
.and_then(|p| p.as_array());
match pools_raw {
Some(pools) => {
let limited: Vec<Value> = pools
.iter()
.take(limit)
.map(|p| {
let token0 = p["token0"]["symbol"].as_str().unwrap_or("?");
let token1 = p["token1"]["symbol"].as_str().unwrap_or("?");
let tvl = p["totalValueLockedUSD"].as_str().unwrap_or("0");
let vol = p["volumeUSD"].as_str().unwrap_or("0");
let fees = p["feesUSD"].as_str().unwrap_or("0");
let id = p["id"].as_str().unwrap_or("");
serde_json::json!({
"pair": format!("{}/{}", token0, token1),
"token0": token0,
"token1": token1,
"address": id,
"tvlUSD": format_usd(tvl),
"volumeUSD": format_usd(vol),
"feesUSD": format_usd(fees)
})
})
.collect();
Ok(serde_json::json!({
"ok": true,
"source": "subgraph",
"count": limited.len(),
"pools": limited
}))
}
None => Ok(subgraph_unavailable_response(limit)),
}
}
Err(_) => Ok(subgraph_unavailable_response(limit)),
}
}
fn subgraph_unavailable_response(limit: usize) -> Value {
let fallback = fallback_pools();
let limited: Vec<Value> = fallback.into_iter().take(limit).collect();
serde_json::json!({
"ok": true,
"note": "subgraph unavailable — showing hardcoded top pools",
"source": "fallback",
"count": limited.len(),
"pools": limited,
"subgraphUrl": SUBGRAPH_URL
})
}
fn format_usd(raw: &str) -> String {
let val: f64 = raw.parse().unwrap_or(0.0);
if val >= 1_000_000.0 {
format!("${:.2}M", val / 1_000_000.0)
} else if val >= 1_000.0 {
format!("${:.2}K", val / 1_000.0)
} else {
format!("${:.2}", val)
}
}
use anyhow::Context;
use serde_json::Value;
use crate::config::{CHAIN_NAME, RPC_URL, WMATIC};
use crate::onchainos;
use crate::rpc;
pub async fn run(
chain_id: u64,
token_in: &str,
token_out: &str,
amount: f64,
) -> anyhow::Result<Value> {
if amount <= 0.0 {
return Err(anyhow::anyhow!("Amount must be greater than 0"));
}
// Resolve tokens
let (in_addr, in_decimals) = onchainos::resolve_token(token_in, chain_id)
.await
.with_context(|| format!("Failed to resolve tokenIn '{}'", token_in))?;
let (out_addr, out_decimals) = onchainos::resolve_token(token_out, chain_id)
.await
.with_context(|| format!("Failed to resolve tokenOut '{}'", token_out))?;
// Get symbols for display
let in_symbol = get_display_symbol(token_in, &in_addr).await;
let out_symbol = get_display_symbol(token_out, &out_addr).await;
// Convert amount to smallest units
let in_scale = 10u128.pow(in_decimals as u32);
let amount_in_raw = (amount * in_scale as f64) as u128;
if amount_in_raw == 0 {
return Err(anyhow::anyhow!(
"Amount {} is too small for {} (decimals: {})",
amount,
in_symbol,
in_decimals
));
}
// Get quote from Quoter contract
let amount_out_raw = rpc::quote_exact_input_single(&in_addr, &out_addr, amount_in_raw, RPC_URL)
.await
.with_context(|| {
format!(
"Quote failed for {}/{} — pool may not exist",
in_symbol, out_symbol
)
})?;
if amount_out_raw == 0 {
return Err(anyhow::anyhow!(
"Amount {:.6} {} is too small — Quoter returned 0 output. Try a larger amount.",
amount, in_symbol
));
}
// Format output amounts
let out_scale = 10u128.pow(out_decimals as u32);
let amount_out = amount_out_raw as f64 / out_scale as f64;
let price = if amount > 0.0 { amount_out / amount } else { 0.0 };
Ok(serde_json::json!({
"ok": true,
"tokenIn": in_symbol,
"tokenInAddress": in_addr,
"tokenInDecimals": in_decimals,
"tokenOut": out_symbol,
"tokenOutAddress": out_addr,
"tokenOutDecimals": out_decimals,
"amountIn": format!("{:.6}", amount),
"amountOut": format!("{:.6}", amount_out),
"price": format!("{:.6}", price),
"chain": CHAIN_NAME,
"note": "Price is indicative. Actual swap price may differ due to price impact."
}))
}
/// Get a display-friendly symbol: use original input if it's a readable symbol,
/// otherwise fall back to RPC symbol lookup.
async fn get_display_symbol(input: &str, addr: &str) -> String {
let upper = input.trim().to_uppercase();
// If it's a known symbol name (not an address), use it
if !input.starts_with("0x") {
// Normalize MATIC/POL/WPOL to WMATIC for display
if upper == "MATIC" || upper == "POL" || upper == "WPOL" {
return "WMATIC".to_string();
}
return input.trim().to_uppercase();
}
// It's an address — look up symbol from RPC; shorten address if lookup fails or returns empty
let sym = rpc::get_erc20_symbol(addr, RPC_URL)
.await
.unwrap_or_default();
if sym.is_empty() || sym == "UNKNOWN" {
if addr.len() > 10 {
format!("0x{}...{}", &addr[2..6], &addr[addr.len() - 4..])
} else {
addr.to_string()
}
} else {
sym
}
}
/// Normalize MATIC/POL to WMATIC display name
#[allow(dead_code)]
fn normalize_display_name(input: &str) -> String {
let upper = input.to_uppercase();
if upper == "MATIC" || upper == "POL" || upper == "WPOL" {
return "WMATIC".to_string();
}
upper
}
use anyhow::Context;
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::calldata;
use crate::config::{CHAIN_ID, CHAIN_NAME, DEADLINE_SECS, RPC_URL, SWAP_ROUTER, WMATIC};
use crate::onchainos;
use crate::rpc;
pub async fn run(
chain_id: u64,
token_in: &str,
token_out: &str,
amount: f64,
slippage: f64,
from: Option<&str>,
dry_run: bool,
) -> anyhow::Result<Value> {
// --- Validation ---
if amount <= 0.0 {
return Err(anyhow::anyhow!("Amount must be greater than 0"));
}
if slippage < 0.0 || slippage > 50.0 {
return Err(anyhow::anyhow!(
"Slippage must be between 0 and 50 (got {}%)",
slippage
));
}
// --- Resolve tokens ---
let (in_addr, in_decimals) = onchainos::resolve_token(token_in, chain_id)
.await
.with_context(|| format!("Failed to resolve tokenIn '{}'", token_in))?;
let (out_addr, out_decimals) = onchainos::resolve_token(token_out, chain_id)
.await
.with_context(|| format!("Failed to resolve tokenOut '{}'", token_out))?;
let in_symbol = display_symbol(token_in, &in_addr);
let out_symbol = display_symbol(token_out, &out_addr);
// --- Resolve wallet address ---
let wallet = match from {
Some(f) => f.to_lowercase(),
None => onchainos::wallet_address(chain_id)
.with_context(|| "Could not determine wallet address")?,
};
// --- Convert amount to raw units ---
let in_scale = 10u128.pow(in_decimals as u32);
let amount_in_raw = (amount * in_scale as f64) as u128;
if amount_in_raw == 0 {
return Err(anyhow::anyhow!(
"Amount {} is too small for {} (decimals: {})",
amount,
in_symbol,
in_decimals
));
}
// --- Get quote ---
let amount_out_raw =
rpc::quote_exact_input_single(&in_addr, &out_addr, amount_in_raw, RPC_URL)
.await
.with_context(|| {
format!(
"Quote failed for {}/{} — pool may not exist on QuickSwap V3",
in_symbol, out_symbol
)
})?;
// Guard: if quote returns 0 output, the amount is too small to route
if amount_out_raw == 0 {
return Err(anyhow::anyhow!(
"Amount {:.6} {} is too small — Quoter returned 0 output. \
Try a larger amount (minimum ~0.01 {} recommended).",
amount, in_symbol, in_symbol
));
}
// --- Compute amountOutMin with slippage ---
let slippage_factor = 1.0 - (slippage / 100.0);
let amount_out_min_raw = (amount_out_raw as f64 * slippage_factor) as u128;
let out_scale = 10u128.pow(out_decimals as u32);
let amount_out_human = amount_out_raw as f64 / out_scale as f64;
let amount_out_min_human = amount_out_min_raw as f64 / out_scale as f64;
// --- Deadline ---
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let deadline = now + DEADLINE_SECS;
// Is this a native MATIC input? (tokenIn resolves to WMATIC but user supplied MATIC/POL)
let is_native_matic = {
let upper = token_in.trim().to_uppercase();
upper == "MATIC" || upper == "POL"
};
// Dry-run preview: show all planned steps without broadcasting
if dry_run {
let mut steps = Vec::new();
if is_native_matic {
steps.push(serde_json::json!({
"step": 1,
"action": "wrap",
"description": format!("Wrap {:.6} MATIC → WMATIC (deposit)", amount),
"contract": WMATIC,
"value_wei": amount_in_raw.to_string()
}));
}
let approve_data = calldata::encode_erc20_approve(SWAP_ROUTER, amount_in_raw);
steps.push(serde_json::json!({
"step": if is_native_matic { 2 } else { 1 },
"action": "approve",
"description": format!("Approve SwapRouter to spend {:.6} {}", amount, in_symbol),
"contract": in_addr,
"spender": SWAP_ROUTER,
"calldata": &approve_data[..approve_data.len().min(66)]
}));
let swap_data = calldata::encode_exact_input_single(
&in_addr,
&out_addr,
&wallet,
deadline,
amount_in_raw,
amount_out_min_raw,
);
steps.push(serde_json::json!({
"step": if is_native_matic { 3 } else { 2 },
"action": "swap",
"description": format!("Swap {:.6} {} → min {:.6} {} on QuickSwap V3",
amount, in_symbol, amount_out_min_human, out_symbol),
"contract": SWAP_ROUTER,
"calldata": &swap_data[..swap_data.len().min(66)]
}));
return Ok(serde_json::json!({
"ok": true,
"preview": true,
"message": "Dry-run preview. Pass --confirm to execute on-chain.",
"wallet": wallet,
"tokenIn": in_symbol,
"tokenOut": out_symbol,
"amountIn": format!("{:.6}", amount),
"amountOutEstimated": format!("{:.6}", amount_out_human),
"amountOutMinimum": format!("{:.6}", amount_out_min_human),
"slippage": format!("{}%", slippage),
"deadline": deadline,
"chain": CHAIN_NAME,
"steps": steps
}));
}
// --- Live execution ---
let mut tx_log: Vec<Value> = Vec::new();
// Pre-flight: check wallet balance for ERC-20 tokens (not MATIC wraps — native balance checked separately)
if !is_native_matic {
let balance = rpc::get_erc20_balance(&in_addr, &wallet, RPC_URL)
.await
.unwrap_or(0);
if balance < amount_in_raw {
let in_scale = 10u128.pow(in_decimals as u32);
return Err(anyhow::anyhow!(
"Insufficient {} balance: need {:.6}, have {:.6}. \
Check the token address — you may have USDC.e (0x2791...) instead of native USDC (0x3c49...).",
in_symbol,
amount,
balance as f64 / in_scale as f64
));
}
}
// Step 1: Wrap MATIC → WMATIC if needed
if is_native_matic {
let wrap_data = calldata::encode_wmatic_deposit();
eprintln!("[1/3] Wrapping {:.6} MATIC → WMATIC...", amount);
let wrap_result = onchainos::wallet_contract_call_with_value(
CHAIN_ID,
WMATIC,
&wrap_data,
Some(&wallet),
amount_in_raw,
false,
)
.context("WMATIC wrap (deposit) failed")?;
let wrap_hash = extract_tx_hash(&wrap_result);
tx_log.push(serde_json::json!({
"step": "wrap",
"txHash": wrap_hash
}));
if let Some(hash) = &wrap_hash {
eprintln!(" Waiting for wrap tx {}...", hash);
rpc::wait_for_tx(RPC_URL, hash)
.await
.context("WMATIC wrap tx did not confirm")?;
eprintln!(" Wrap confirmed.");
}
}
// Step 2: Check and approve allowance
let current_allowance = rpc::get_allowance(&in_addr, &wallet, SWAP_ROUTER, RPC_URL)
.await
.unwrap_or(0);
if current_allowance < amount_in_raw {
let step_num = if is_native_matic { 2 } else { 1 };
eprintln!(
"[{}/{}] Approving SwapRouter to spend {} {}...",
step_num,
if is_native_matic { 3 } else { 2 },
amount,
in_symbol
);
// Approve exact operation amount (scoped — not unlimited)
let approve_data = calldata::encode_erc20_approve(SWAP_ROUTER, amount_in_raw);
let approve_result = onchainos::wallet_contract_call(
CHAIN_ID,
&in_addr,
&approve_data,
Some(&wallet),
false,
)
.context("ERC-20 approval failed")?;
let approve_hash = extract_tx_hash(&approve_result);
tx_log.push(serde_json::json!({
"step": "approve",
"txHash": approve_hash
}));
if let Some(hash) = &approve_hash {
eprintln!(" Waiting for approve tx {}...", hash);
rpc::wait_for_tx(RPC_URL, hash)
.await
.context("Approval tx did not confirm")?;
eprintln!(" Approval confirmed.");
}
} else {
eprintln!(" Allowance sufficient — skipping approve.");
tx_log.push(serde_json::json!({
"step": "approve",
"skipped": true,
"reason": "existing allowance sufficient"
}));
}
// Step 3: Swap
let swap_step = if is_native_matic { 3 } else { 2 };
eprintln!(
"[{}/{}] Swapping {:.6} {} → {} on QuickSwap V3...",
swap_step,
swap_step,
amount,
in_symbol,
out_symbol
);
let swap_data = calldata::encode_exact_input_single(
&in_addr,
&out_addr,
&wallet,
deadline,
amount_in_raw,
amount_out_min_raw,
);
let swap_result = onchainos::wallet_contract_call(
CHAIN_ID,
SWAP_ROUTER,
&swap_data,
Some(&wallet),
false,
)
.context("Swap transaction failed")?;
let swap_hash = extract_tx_hash(&swap_result);
let explorer_link = swap_hash.as_ref().map(|h| {
format!("https://polygonscan.com/tx/{}", h)
});
tx_log.push(serde_json::json!({
"step": "swap",
"txHash": swap_hash,
"explorerLink": explorer_link
}));
Ok(serde_json::json!({
"ok": true,
"tokenIn": in_symbol,
"tokenOut": out_symbol,
"amountIn": format!("{:.6}", amount),
"amountOutEstimated": format!("{:.6}", amount_out_human),
"amountOutMinimum": format!("{:.6}", amount_out_min_human),
"slippage": format!("{}%", slippage),
"wallet": wallet,
"chain": CHAIN_NAME,
"transactions": tx_log,
"explorerLink": explorer_link
}))
}
fn display_symbol(input: &str, addr: &str) -> String {
if input.starts_with("0x") && input.len() == 42 {
// Shorten address for display: 0x2791...4174
return format!("0x{}...{}", &addr[2..6], &addr[addr.len().saturating_sub(4)..]);
}
let upper = input.trim().to_uppercase();
if upper == "MATIC" || upper == "POL" || upper == "WPOL" {
return "WMATIC".to_string();
}
upper
}
fn extract_tx_hash(result: &Value) -> Option<String> {
// onchainos returns: {"ok":true,"data":{"txHash":"0x...","orderId":"..."}}
if let Some(h) = result["data"]["txHash"].as_str() {
if h.starts_with("0x") && h.len() == 66 {
return Some(h.to_string());
}
}
// Flat fallbacks for other response shapes
for key in &["txHash", "tx_hash", "hash"] {
if let Some(h) = result.get(key).and_then(|v| v.as_str()) {
if h.starts_with("0x") && h.len() == 66 {
return Some(h.to_string());
}
}
}
if let Some(h) = result.get("result").and_then(|v| v.as_str()) {
if h.starts_with("0x") && h.len() == 66 {
return Some(h.to_string());
}
}
None
}
pub const CHAIN_ID: u64 = 137;
pub const CHAIN_NAME: &str = "Polygon";
pub const RPC_URL: &str = "https://polygon.publicnode.com";
pub const SWAP_ROUTER: &str = "0xf5b509bb0909a69b1c207e495f687a596c168e12";
pub const QUOTER: &str = "0xa15F0D7377B2A0C0c10db057f641beD21028FC89";
pub const WMATIC: &str = "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270";
pub const DEFAULT_SLIPPAGE_BPS: u64 = 50; // 0.5%
pub const DEADLINE_SECS: u64 = 1200; // 20 minutes
mod calldata;
mod commands;
mod config;
mod onchainos;
mod rpc;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "quickswap-plugin",
version = env!("CARGO_PKG_VERSION"),
about = "QuickSwap V3 DEX on Polygon — swap tokens via Algebra Protocol CLMM"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
/// Broadcast on-chain (omit for dry-run preview)
#[arg(long, global = true)]
confirm: bool,
}
#[derive(Subcommand)]
enum Commands {
/// Swap tokens on QuickSwap V3 (Algebra Protocol CLMM)
Swap {
/// Input token symbol or address (e.g. MATIC, USDC, 0x...)
#[arg(long)]
token_in: String,
/// Output token symbol or address (e.g. USDC, WETH, 0x...)
#[arg(long)]
token_out: String,
/// Amount of tokenIn to swap (human-readable, e.g. 10.5)
#[arg(long)]
amount: f64,
/// Maximum slippage tolerance in percent (default: 0.5)
#[arg(long, default_value = "0.5")]
slippage: f64,
/// Override sender address (defaults to onchainos wallet)
#[arg(long)]
from: Option<String>,
},
/// Get a swap quote without executing a transaction
Quote {
/// Input token symbol or address
#[arg(long)]
token_in: String,
/// Output token symbol or address
#[arg(long)]
token_out: String,
/// Amount of tokenIn (human-readable)
#[arg(long)]
amount: f64,
},
/// List top QuickSwap V3 pools by TVL
Pools {
/// Number of pools to return (default: 10)
#[arg(long, default_value = "10")]
limit: usize,
},
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let dry_run = !cli.confirm;
let result = match cli.command {
Commands::Swap {
token_in,
token_out,
amount,
slippage,
from,
} => {
commands::swap::run(
config::CHAIN_ID,
&token_in,
&token_out,
amount,
slippage,
from.as_deref(),
dry_run,
)
.await
}
Commands::Quote {
token_in,
token_out,
amount,
} => commands::quote::run(config::CHAIN_ID, &token_in, &token_out, amount).await,
Commands::Pools { limit } => commands::pools::run(limit).await,
};
match result {
Ok(val) => {
println!("{}", serde_json::to_string_pretty(&val).unwrap());
std::process::exit(0);
}
Err(e) => {
let err = serde_json::json!({"error": e.to_string()});
eprintln!("{}", serde_json::to_string_pretty(&err).unwrap());
std::process::exit(1);
}
}
}
use anyhow::Context;
use serde_json::Value;
use std::process::Command;
use crate::config::WMATIC;
use crate::rpc;
/// Normalize a token symbol/address to (address, decimals).
/// - If it looks like an address (0x + 42 chars), return as-is with RPC decimals lookup.
/// - If "MATIC", "POL", "WPOL" → return WMATIC address.
/// - Otherwise, call `onchainos token search --chain polygon --symbol <sym>`.
pub async fn resolve_token(
symbol_or_addr: &str,
_chain_id: u64,
) -> anyhow::Result<(String, u8)> {
let input = symbol_or_addr.trim();
// Native MATIC / POL / WPOL → WMATIC
let upper = input.to_uppercase();
if upper == "MATIC" || upper == "POL" || upper == "WPOL" || upper == "WMATIC" {
let decimals = rpc::get_erc20_decimals(WMATIC, crate::config::RPC_URL)
.await
.unwrap_or(18);
return Ok((WMATIC.to_lowercase(), decimals));
}
// Already an address
if input.starts_with("0x") && input.len() == 42 {
let decimals = rpc::get_erc20_decimals(input, crate::config::RPC_URL)
.await
.unwrap_or(18);
return Ok((input.to_lowercase(), decimals));
}
// Resolve via onchainos token search (uses --query flag, returns {"ok":true,"data":[...]})
let output = Command::new("onchainos")
.args([
"token",
"search",
"--chain",
"polygon",
"--query",
input,
])
.output()
.context("Failed to run onchainos token search")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let val: Value = serde_json::from_str(&stdout)
.map_err(|_| anyhow::anyhow!("Could not resolve token '{}'. Check the symbol and try again, or pass the token address directly.", input))?;
// Response: {"ok": true, "data": [{...token...}, ...]}
let token = val["data"]
.as_array()
.and_then(|arr| arr.first())
.cloned()
.or_else(|| {
// Fallback: flat array at root
val.as_array().and_then(|arr| arr.first()).cloned()
})
.ok_or_else(|| anyhow::anyhow!("Token '{}' not found. Use the token address directly (e.g. 0x2791Bca1...) or check the symbol spelling.", input))?;
// Field names from onchainos: tokenContractAddress, decimal (not address/decimals)
let address = token["tokenContractAddress"]
.as_str()
.or_else(|| token["address"].as_str())
.ok_or_else(|| anyhow::anyhow!("Token '{}' resolved but has no address field", input))?
.to_lowercase();
let decimals = token["decimal"]
.as_str()
.and_then(|s| s.parse::<u64>().ok())
.or_else(|| token["decimals"].as_u64())
.unwrap_or(18) as u8;
Ok((address, decimals))
}
/// Get the user's wallet address for a given chain.
pub fn wallet_address(chain_id: u64) -> anyhow::Result<String> {
let output = Command::new("onchainos")
.args([
"wallet",
"addresses",
"--chain",
&chain_id.to_string(),
])
.output()
.context("Failed to run onchainos wallet addresses")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let val: Value = serde_json::from_str(&stdout)
.map_err(|_| anyhow::anyhow!("Failed to parse wallet addresses response"))?;
// Response: {"ok":true,"data":{"evm":[{"address":"0x...","chainIndex":"137",...}],...}}
let addr = val["data"]["evm"]
.as_array()
.and_then(|arr| arr.first())
.and_then(|v| v["address"].as_str())
.map(|s| s.to_string())
// Fallback: flat array or simple {"address":"..."}
.or_else(|| {
val.as_array()
.and_then(|arr| arr.first())
.and_then(|v| v["address"].as_str().or_else(|| v.as_str()))
.map(|s| s.to_string())
})
.or_else(|| val["address"].as_str().map(|s| s.to_string()));
addr.ok_or_else(|| anyhow::anyhow!("No wallet address found for chain {}. Run: onchainos wallet addresses --chain {}", chain_id, chain_id))
}
/// Execute a contract call via onchainos.
/// dry_run = true → return a preview JSON without broadcasting.
pub fn wallet_contract_call(
chain_id: u64,
to: &str,
data: &str,
from: Option<&str>,
dry_run: bool,
) -> anyhow::Result<Value> {
if dry_run {
return Ok(serde_json::json!({
"dryRun": true,
"to": to,
"inputData": &data[..data.len().min(66)],
"chain": chain_id,
"status": "preview — pass --confirm to broadcast"
}));
}
let mut args = vec![
"wallet".to_string(),
"contract-call".to_string(),
"--chain".to_string(),
chain_id.to_string(),
"--to".to_string(),
to.to_string(),
"--input-data".to_string(),
data.to_string(),
];
if let Some(f) = from {
args.push("--from".to_string());
args.push(f.to_string());
}
args.push("--biz-type".to_string());
args.push("dapp".to_string());
args.push("--strategy".to_string());
args.push("quickswap-plugin".to_string());
let output = Command::new("onchainos")
.args(&args)
.output()
.context("Failed to run onchainos wallet contract-call")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
return Err(anyhow::anyhow!(
"onchainos contract-call failed: {}",
stderr.trim()
));
}
serde_json::from_str(&stdout)
.map_err(|_| anyhow::anyhow!("Failed to parse contract-call response: {}", stdout.trim()))
}
/// Execute a payable contract call with a native value amount (in wei).
pub fn wallet_contract_call_with_value(
chain_id: u64,
to: &str,
data: &str,
from: Option<&str>,
value: u128,
dry_run: bool,
) -> anyhow::Result<Value> {
if dry_run {
return Ok(serde_json::json!({
"dryRun": true,
"to": to,
"inputData": &data[..data.len().min(66)],
"value": value.to_string(),
"chain": chain_id,
"status": "preview — pass --confirm to broadcast"
}));
}
let mut args = vec![
"wallet".to_string(),
"contract-call".to_string(),
"--chain".to_string(),
chain_id.to_string(),
"--to".to_string(),
to.to_string(),
"--input-data".to_string(),
data.to_string(),
"--amt".to_string(),
value.to_string(),
];
if let Some(f) = from {
args.push("--from".to_string());
args.push(f.to_string());
}
args.push("--biz-type".to_string());
args.push("dapp".to_string());
args.push("--strategy".to_string());
args.push("quickswap-plugin".to_string());
let output = Command::new("onchainos")
.args(&args)
.output()
.context("Failed to run onchainos wallet contract-call")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
return Err(anyhow::anyhow!(
"onchainos contract-call (payable) failed: {}",
stderr.trim()
));
}
serde_json::from_str(&stdout)
.map_err(|_| anyhow::anyhow!("Failed to parse contract-call response: {}", stdout.trim()))
}
use anyhow::Context;
use reqwest::Client;
use serde_json::{json, Value};
use tokio::time::{sleep, Duration};
use crate::calldata;
use crate::config::QUOTER;
async fn eth_call(to: &str, data: &str, rpc: &str) -> anyhow::Result<String> {
let client = Client::new();
let body = json!({
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{"to": to, "data": data}, "latest"],
"id": 1
});
let resp = client
.post(rpc)
.json(&body)
.send()
.await
.context("RPC request failed")?;
let val: Value = resp.json().await.context("RPC response parse failed")?;
if let Some(err) = val.get("error") {
return Err(anyhow::anyhow!("RPC error: {}", err));
}
Ok(val["result"].as_str().unwrap_or("0x").to_string())
}
pub async fn get_erc20_balance(token: &str, account: &str, rpc: &str) -> anyhow::Result<u128> {
let data = calldata::encode_balance_of(account);
let result = eth_call(token, &data, rpc).await?;
calldata::decode_u128(&result).context("Failed to decode balance")
}
pub async fn get_erc20_decimals(token: &str, rpc: &str) -> anyhow::Result<u8> {
let data = calldata::encode_decimals();
let result = eth_call(token, &data, rpc).await?;
calldata::decode_u8(&result).context("Failed to decode decimals")
}
pub async fn get_erc20_symbol(token: &str, rpc: &str) -> anyhow::Result<String> {
let data = calldata::encode_symbol();
let result = eth_call(token, &data, rpc).await?;
calldata::decode_string(&result).context("Failed to decode symbol")
}
pub async fn get_allowance(
token: &str,
owner: &str,
spender: &str,
rpc: &str,
) -> anyhow::Result<u128> {
let data = calldata::encode_allowance(owner, spender);
let result = eth_call(token, &data, rpc).await?;
calldata::decode_u128(&result).context("Failed to decode allowance")
}
pub async fn get_matic_balance(account: &str, rpc: &str) -> anyhow::Result<u128> {
let client = Client::new();
let body = json!({
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": [account, "latest"],
"id": 1
});
let resp = client
.post(rpc)
.json(&body)
.send()
.await
.context("eth_getBalance request failed")?;
let val: Value = resp.json().await.context("eth_getBalance parse failed")?;
if let Some(err) = val.get("error") {
return Err(anyhow::anyhow!("RPC error: {}", err));
}
let hex_str = val["result"].as_str().unwrap_or("0x0");
calldata::decode_u128(hex_str).context("Failed to decode MATIC balance")
}
pub async fn quote_exact_input_single(
token_in: &str,
token_out: &str,
amount_in: u128,
rpc: &str,
) -> anyhow::Result<u128> {
let data = calldata::encode_quote_exact_input_single(token_in, token_out, amount_in);
let result = eth_call(QUOTER, &data, rpc).await.map_err(|e| {
anyhow::anyhow!(
"Quoter call failed (pool may not exist for this pair): {}",
e
)
})?;
if result == "0x" || result.is_empty() {
return Err(anyhow::anyhow!(
"Quoter returned empty result — no pool exists for this token pair on QuickSwap V3"
));
}
// Result is (uint256 amountOut, uint16 fee) packed in 64 bytes.
// decode_u128 reads the LAST 16 bytes so we must truncate to first 32 bytes (= first 64 hex chars + "0x").
let first32 = if result.len() > 66 {
result[..66].to_string()
} else {
result.clone()
};
calldata::decode_u128(&first32).context("Failed to decode quote amountOut")
}
pub async fn wait_for_tx(rpc: &str, tx_hash: &str) -> anyhow::Result<()> {
let client = Client::new();
for _ in 0..30 {
sleep(Duration::from_secs(2)).await;
let body = json!({
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": [tx_hash],
"id": 1
});
let resp = client
.post(rpc)
.json(&body)
.send()
.await
.context("eth_getTransactionReceipt request failed")?;
let val: Value = resp.json().await.context("receipt parse failed")?;
if let Some(receipt) = val.get("result") {
if !receipt.is_null() {
// Check status
if let Some(status) = receipt.get("status") {
let s = status.as_str().unwrap_or("0x0");
if s == "0x0" {
return Err(anyhow::anyhow!("Transaction {} reverted", tx_hash));
}
}
return Ok(());
}
}
}
Err(anyhow::anyhow!(
"Transaction {} not confirmed after 60 seconds",
tx_hash
))
}
QuickSwap DEX — Plugin Summary
Version: 0.1.2 Chain: Polygon (chain ID 137) Protocol: QuickSwap V3 (Algebra Protocol CLMM — no fee tiers)
Overview
QuickSwap V3 is a concentrated liquidity DEX on Polygon built on Algebra Protocol. Unlike Uniswap V3, it uses dynamic fees with no explicit fee tier parameter. This plugin enables token swaps via the SwapRouter, price quotes via the on-chain Quoter, and pool discovery via the QuickSwap subgraph.
Key contracts:
- SwapRouter:
0xf5b509bb0909a69b1c207e495f687a596c168e12 - Quoter:
0xa15F0D7377B2A0C0c10db057f641beD21028FC89 - Factory:
0x411b0fAcC3489691f28ad58c47006AF5E3Ab3A28
Prerequisites
- onchainos CLI installed and configured with a Polygon wallet
quickswap-pluginbinary installed (see SKILL.md Install section)- Sufficient MATIC/POL for gas, plus the token you want to swap
Quick Start
# 1. Verify installation
quickswap-plugin --version
# 2. Get a price quote (read-only, no wallet needed)
quickswap-plugin quote --token-in MATIC --token-out USDC --amount 10
# 3. Preview a swap (dry-run, no transaction)
quickswap-plugin swap --token-in MATIC --token-out USDC --amount 10
# 4. Execute the swap on-chain
quickswap-plugin swap --token-in MATIC --token-out USDC --amount 10 --confirm
# 5. Browse top pools
quickswap-plugin pools --limit 10