diff --git a/symbiosis/SKILL.md b/symbiosis/SKILL.md new file mode 100644 index 00000000..84889dc5 --- /dev/null +++ b/symbiosis/SKILL.md @@ -0,0 +1,156 @@ +--- +name: symbiosis +description: Cross-chain token swaps across 54+ blockchains via Symbiosis protocol. Use when the user wants to swap or bridge tokens between any chains — Base, Ethereum, Polygon, Arbitrum, Optimism, BNB Chain, Avalanche, Solana, Bitcoin, TON, Tron, and 40+ more. Supports any-to-any token swaps with automatic routing. Uses Bankr Submit API to execute transactions. +metadata: + { + "clawdbot": + { + "emoji": "🔀", + "homepage": "https://symbiosis.finance", + "requires": { "bins": ["python3", "bankr"] }, + }, + } +--- + +# Symbiosis + +Cross-chain token swaps across 54+ blockchains. Swap any token on any chain to any token on any other chain. + +## When To Use + +Use Symbiosis when the user wants to: +- **Bridge or swap tokens between different chains** (e.g., USDC from Base to Polygon, ETH from Ethereum to Arbitrum) +- **Access chains beyond Bankr's native 5** (Arbitrum, Optimism, BNB Chain, Avalanche, zkSync, Linea, Scroll, Mantle, Blast, and 40+ more) +- **Swap to/from Bitcoin, TON, or Tron** +- **Get a cross-chain quote** without executing + +## Quick Start + +### Get a Quote + +``` +How much USDC will I get on Polygon if I bridge 10 USDC from Base? +``` + +Run the quote script: + +```bash +scripts/symbiosis-quote.py 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 10 137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 6 +``` + +### Execute a Swap + +``` +Bridge 2 USDC from Base to Polygon using Symbiosis +``` + +Run the swap script: + +```bash +scripts/symbiosis-swap.py 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 2 137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 6 +``` + +## Script Usage + +### symbiosis-swap.py + +Executes a full cross-chain swap: gets quote from Symbiosis API, approves token if needed, submits swap transaction via Bankr Submit API. + +``` +scripts/symbiosis-swap.py [slippage] +``` + +- `amount` — human-readable (e.g., "2" for 2 USDC, "0.1" for 0.1 ETH) +- `slippage` — optional, in basis points (default: 200 = 2%) +- Reads Bankr API key from `~/.bankr/config.json` +- Automatically gets wallet address from Bankr +- Outputs transaction hash and Explorer tracking link + +### symbiosis-quote.py + +Gets a quote without executing. Same arguments, no slippage parameter. + +``` +scripts/symbiosis-quote.py +``` + +## Common Chains and Tokens + +### Bankr Wallet Chains + +| Chain | ID | USDC Address | USDC Dec | +|-------|----|-------------|----------| +| Base | 8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 | +| Ethereum | 1 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | 6 | +| Polygon | 137 | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | 6 | + +### Additional Chains via Symbiosis + +| Chain | ID | USDC Address | USDC Dec | +|-------|----|-------------|----------| +| Arbitrum | 42161 | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 | 6 | +| Optimism | 10 | 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 | 6 | +| BNB Chain | 56 | 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d | 18 | +| Avalanche | 43114 | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E | 6 | + +### Native Tokens + +Use `0x0000000000000000000000000000000000000000` as address for native gas tokens (ETH, POL, BNB, AVAX, etc.). + +**Reference**: [references/chains-and-tokens.md](references/chains-and-tokens.md) for the full list. + +## Examples + +### EVM to EVM + +```bash +# 5 USDC: Base -> Arbitrum +scripts/symbiosis-swap.py 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 5 42161 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 6 + +# 0.01 ETH: Ethereum -> Base +scripts/symbiosis-swap.py 1 0x0000000000000000000000000000000000000000 18 0.01 8453 0x0000000000000000000000000000000000000000 18 + +# 10 USDC: Polygon -> BNB Chain +scripts/symbiosis-swap.py 137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 6 10 56 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d 18 + +# 0.5 ETH: Base -> Optimism +scripts/symbiosis-swap.py 8453 0x0000000000000000000000000000000000000000 18 0.5 10 0x0000000000000000000000000000000000000000 18 +``` + +### Cross-ecosystem (Symbiosis-only routes) + +```bash +# 10 USDC: Base -> Solana +# Note: Solana chain ID in Symbiosis is 5426 +# Solana USDC: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (but use Symbiosis synthetic address) +scripts/symbiosis-swap.py 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 10 5426 0x0000000000000000000000000000000000000000 9 +``` + +### Prompt Examples + +Users might say: + +- "Bridge 5 USDC from Base to Arbitrum" +- "Swap 0.1 ETH from Ethereum to Polygon" +- "Move my USDC from Base to Optimism" +- "How much will I get if I bridge 100 USDC from Base to Avalanche?" +- "Cross-chain swap 50 USDC from Polygon to BNB Chain" +- "Bridge ETH from Base to Solana" + +For each request: identify source chain + token, destination chain + token, look up chain IDs and token addresses from the tables above, and run the appropriate script. + +## How It Works + +1. **Quote**: Script calls Symbiosis API (`POST /crosschain/v1/swap`) with token details and wallet address +2. **Approve**: If source token needs approval, script submits an ERC20 approve transaction via `POST https://api.bankr.bot/agent/submit` +3. **Swap**: Script submits the swap transaction via `POST https://api.bankr.bot/agent/submit` +4. **Track**: Returns an Explorer link for cross-chain status tracking + +All transactions are submitted through the Bankr Submit API using the user's Bankr wallet. No additional wallets or keys needed. + +## Resources + +- **Explorer**: https://explorer.symbiosis.finance +- **Website**: https://symbiosis.finance +- **API Docs**: [references/api-reference.md](references/api-reference.md) +- **Chains & Tokens**: [references/chains-and-tokens.md](references/chains-and-tokens.md) diff --git a/symbiosis/references/api-reference.md b/symbiosis/references/api-reference.md new file mode 100644 index 00000000..ada7fba3 --- /dev/null +++ b/symbiosis/references/api-reference.md @@ -0,0 +1,105 @@ +# Symbiosis API Reference + +Base URL: `https://api-v2.symbiosis.finance` + +## POST /crosschain/v1/swap + +Get a cross-chain swap quote with executable calldata. + +### Request + +```json +{ + "tokenAmountIn": { + "chainId": 8453, + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "decimals": 6, + "amount": "2000000" + }, + "tokenOut": { + "chainId": 137, + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "decimals": 6 + }, + "from": "0xUserAddress", + "to": "0xUserAddress", + "slippage": 200, + "partnerId": "bankr" +} +``` + +**Fields:** +- `tokenAmountIn.amount` — amount in smallest units (e.g., "2000000" for 2 USDC with 6 decimals) +- `slippage` — in basis points (200 = 2%, recommended default) +- `partnerId` — always use "bankr" +- `from` / `to` — user's wallet addresses (can be different for cross-chain) + +### Response + +```json +{ + "tx": { + "to": "0x691df9C4561d95a4a726313089c8536dd682b946", + "data": "0x...", + "value": "0", + "chainId": 8453 + }, + "approveTo": "0x41Ae964d0F61Bb5F5e253141A462aD6F3b625B92", + "tokenAmountOut": { + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "amount": "1497245", + "chainId": 137, + "decimals": 6 + }, + "fee": { + "amount": "250000", + "decimals": 6, + "priceUsd": 1.0 + }, + "estimatedTime": 32 +} +``` + +**Key fields:** +- `tx` — the swap transaction to submit on the source chain +- `approveTo` — if present, you must approve this spender before the swap +- `tokenAmountOut.amount` — expected output in smallest units +- `estimatedTime` — seconds until destination chain receives funds + +### Amount Conversion + +| Human Amount | Decimals | Smallest Units | +|-------------|----------|---------------| +| 2 USDC | 6 | "2000000" | +| 0.1 ETH | 18 | "100000000000000000" | +| 100 USDT | 6 | "100000000" | +| 1.5 BNB | 18 | "1500000000000000000" | + +Formula: `amount_smallest = human_amount * 10^decimals` + +## POST /crosschain/v2/swap + +Bitcoin-specific endpoint. Returns a deposit address instead of calldata. + +### Response (Bitcoin) + +```json +{ + "depositAddress": "bc1q...", + "depositAddressExpiry": 1234567890, + "tokenAmountOut": { ... } +} +``` + +The user sends BTC to `depositAddress` before `depositAddressExpiry`. + +## GET /crosschain/v1/chains + +Returns all supported chains. + +## Cross-Chain Status Tracking + +After submitting a swap transaction, track status at: +`https://explorer.symbiosis.finance/transactions//` + +Typical completion: 15-60 seconds for most routes, up to 10 minutes for congested chains. diff --git a/symbiosis/references/chains-and-tokens.md b/symbiosis/references/chains-and-tokens.md new file mode 100644 index 00000000..a491c369 --- /dev/null +++ b/symbiosis/references/chains-and-tokens.md @@ -0,0 +1,73 @@ +# Supported Chains and Common Tokens + +## Chains Supported by Both Bankr and Symbiosis + +| Chain | Chain ID | Native Token | Decimals | +|-------|----------|-------------|----------| +| Base | 8453 | ETH | 18 | +| Ethereum | 1 | ETH | 18 | +| Polygon | 137 | POL | 18 | +| Unichain | 130 | ETH | 18 | +| Solana | 5426 | SOL | 9 | + +## Additional Chains (Symbiosis-only, not native to Bankr) + +| Chain | Chain ID | Native Token | Decimals | +|-------|----------|-------------|----------| +| Arbitrum One | 42161 | ETH | 18 | +| Optimism | 10 | ETH | 18 | +| BNB Chain | 56 | BNB | 18 | +| Avalanche C-Chain | 43114 | AVAX | 18 | +| Gnosis | 100 | xDAI | 18 | +| zkSync Era | 324 | ETH | 18 | +| Linea | 59144 | ETH | 18 | +| Scroll | 534352 | ETH | 18 | +| Mantle | 5000 | MNT | 18 | +| Blast | 81457 | ETH | 18 | +| Mode | 34443 | ETH | 18 | +| Sei | 1329 | SEI | 18 | +| Gravity | 1625 | G | 18 | +| ZetaChain | 7000 | ZETA | 18 | +| Cronos | 25 | CRO | 18 | +| Bitcoin | 3652501241 | BTC | 8 | +| TON | 85918 | TON | 9 | +| Tron | 728126428 | TRX | 6 | + +For the full list of 54+ chains, query: `GET https://api-v2.symbiosis.finance/crosschain/v1/chains` + +## Common Token Addresses + +### USDC + +| Chain | Address | Decimals | +|-------|---------|----------| +| Base (8453) | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 | +| Ethereum (1) | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | 6 | +| Polygon (137) | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | 6 | +| Arbitrum (42161) | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 | 6 | +| Optimism (10) | 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 | 6 | +| BNB Chain (56) | 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d | 18 | +| Avalanche (43114) | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E | 6 | + +### USDT + +| Chain | Address | Decimals | +|-------|---------|----------| +| Ethereum (1) | 0xdAC17F958D2ee523a2206206994597C13D831ec7 | 6 | +| BNB Chain (56) | 0x55d398326f99059fF775485246999027B3197955 | 18 | +| Polygon (137) | 0xc2132D05D31c914a87C6611C10748AEb04B58e8F | 6 | +| Arbitrum (42161) | 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9 | 6 | +| Optimism (10) | 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58 | 6 | +| Avalanche (43114) | 0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7 | 6 | + +### Native Gas Tokens + +Use address `0x0000000000000000000000000000000000000000` with the appropriate decimals for the chain's native token (ETH=18, POL=18, BNB=18, SOL=9, BTC=8, etc.). + +## Bitcoin (Special) + +Bitcoin uses the Symbiosis v2 API (`/crosschain/v2/swap`) and returns a **deposit address** instead of calldata. The user must send BTC to the deposit address manually. The deposit address has an expiration time. + +## Solana (Special) + +Solana inbound swaps are limited to SOL and USDC as destination tokens. Outbound from Solana works for SOL and USDC as source. diff --git a/symbiosis/scripts/symbiosis-quote.py b/symbiosis/scripts/symbiosis-quote.py new file mode 100755 index 00000000..64027e8a --- /dev/null +++ b/symbiosis/scripts/symbiosis-quote.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Symbiosis cross-chain quote (no execution). + +Usage: ./symbiosis-quote.sh + +Example: ./symbiosis-quote.sh 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 100 137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 6 +""" + +import json +import sys +import urllib.request + +SYMBIOSIS_API = "https://api-v2.symbiosis.finance/crosschain/v1/swap" +PARTNER_ID = "bankr" +FAKE_ADDR = "0x1111111111111111111111111111111111111111" + + +def to_smallest_units(amount: str, decimals: int) -> str: + parts = amount.split(".") + integer = parts[0] + frac = parts[1] if len(parts) > 1 else "" + frac = (frac + "0" * decimals)[:decimals] + return str(int(integer + frac)) + + +def format_units(amount_raw: str, decimals: int) -> str: + s = amount_raw.zfill(decimals + 1) + int_part = s[: len(s) - decimals] or "0" + frac_part = s[len(s) - decimals :].rstrip("0") + return f"{int_part}.{frac_part}" if frac_part else int_part + + +def api_post(url: str, payload: dict) -> dict: + req = urllib.request.Request( + url, + data=json.dumps(payload).encode(), + headers={ + "Content-Type": "application/json", + "User-Agent": "symbiosis-bankr-skill/1.0", + }, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read()) + + +def main(): + if len(sys.argv) < 8: + print(__doc__.strip()) + sys.exit(1) + + src_chain = int(sys.argv[1]) + src_token = sys.argv[2] + src_dec = int(sys.argv[3]) + amount = sys.argv[4] + dst_chain = int(sys.argv[5]) + dst_token = sys.argv[6] + dst_dec = int(sys.argv[7]) + + result = api_post(SYMBIOSIS_API, { + "tokenAmountIn": { + "chainId": src_chain, + "address": src_token, + "decimals": src_dec, + "amount": to_smallest_units(amount, src_dec), + }, + "tokenOut": { + "chainId": dst_chain, + "address": dst_token, + "decimals": dst_dec, + }, + "from": FAKE_ADDR, + "to": FAKE_ADDR, + "slippage": 200, + "partnerId": PARTNER_ID, + }) + + if "tx" not in result: + msg = result.get("message", result.get("error", json.dumps(result))) + print(f"ERROR: {msg}", file=sys.stderr) + sys.exit(1) + + out = result.get("tokenAmountOut", {}) + out_human = format_units(out.get("amount", "0"), out.get("decimals", dst_dec)) + + fee = result.get("fee", {}) + fee_human = float(format_units(fee.get("amount", "0"), fee.get("decimals", 6))) + fee_usd = fee_human * fee.get("priceUsd", 1) + + est = result.get("estimatedTime", "?") + + print(f"{amount} -> {out_human} (chain {src_chain} -> {dst_chain})") + print(f"Fee: ~${fee_usd:.4f}") + print(f"Estimated time: {est}s") + + +if __name__ == "__main__": + main() diff --git a/symbiosis/scripts/symbiosis-swap.py b/symbiosis/scripts/symbiosis-swap.py new file mode 100755 index 00000000..3812e5dd --- /dev/null +++ b/symbiosis/scripts/symbiosis-swap.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Symbiosis cross-chain swap via Bankr Submit API. + +Usage: ./symbiosis-swap.sh [slippage] + +Example: ./symbiosis-swap.sh 8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 2 137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 6 + (2 USDC from Base to Polygon) +""" + +import json +import os +import sys +import urllib.request + +SYMBIOSIS_API = "https://api-v2.symbiosis.finance/crosschain/v1/swap" +BANKR_API = "https://api.bankr.bot" +PARTNER_ID = "bankr" +ZERO_ADDR = "0x0000000000000000000000000000000000000000" +MAX_UINT256 = "f" * 64 + + +def to_smallest_units(amount: str, decimals: int) -> str: + parts = amount.split(".") + integer = parts[0] + frac = parts[1] if len(parts) > 1 else "" + frac = (frac + "0" * decimals)[:decimals] + return str(int(integer + frac)) + + +def format_units(amount_raw: str, decimals: int) -> str: + s = amount_raw.zfill(decimals + 1) + int_part = s[: len(s) - decimals] or "0" + frac_part = s[len(s) - decimals :].rstrip("0") + return f"{int_part}.{frac_part}" if frac_part else int_part + + +def api_post(url: str, payload: dict, headers: dict | None = None) -> dict: + hdrs = {"Content-Type": "application/json", "User-Agent": "symbiosis-bankr-skill/1.0"} + if headers: + hdrs.update(headers) + req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers=hdrs) + with urllib.request.urlopen(req, timeout=60) as resp: + return json.loads(resp.read()) + + +def api_get(url: str, headers: dict) -> dict: + hdrs = {"User-Agent": "symbiosis-bankr-skill/1.0"} + hdrs.update(headers) + req = urllib.request.Request(url, headers=hdrs) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read()) + + +def load_bankr_key() -> str: + config_path = os.environ.get("BANKR_CONFIG", os.path.expanduser("~/.bankr/config.json")) + if not os.path.exists(config_path): + print(f"ERROR: Bankr config not found at {config_path}", file=sys.stderr) + sys.exit(1) + with open(config_path) as f: + return json.load(f)["apiKey"] + + +def get_wallet(bankr_key: str) -> str: + result = api_get( + f"{BANKR_API}/agent/balances?chains=base", + {"X-API-Key": bankr_key}, + ) + return result["evmAddress"] + + +def bankr_submit(bankr_key: str, tx: dict, description: str) -> dict: + return api_post( + f"{BANKR_API}/agent/submit", + { + "transaction": tx, + "description": description, + "waitForConfirmation": True, + }, + {"X-API-Key": bankr_key}, + ) + + +def main(): + if len(sys.argv) < 8: + print(__doc__.strip()) + sys.exit(1) + + src_chain = int(sys.argv[1]) + src_token = sys.argv[2] + src_dec = int(sys.argv[3]) + amount = sys.argv[4] + dst_chain = int(sys.argv[5]) + dst_token = sys.argv[6] + dst_dec = int(sys.argv[7]) + slippage = int(sys.argv[8]) if len(sys.argv) > 8 else 200 + + # --- Setup --- + bankr_key = load_bankr_key() + wallet = get_wallet(bankr_key) + amount_wei = to_smallest_units(amount, src_dec) + print(f"Wallet: {wallet}") + print(f"Amount in smallest units: {amount_wei}") + + # --- Step 1: Get quote + calldata from Symbiosis --- + print("\n=== Getting Symbiosis quote ===") + result = api_post(SYMBIOSIS_API, { + "tokenAmountIn": { + "chainId": src_chain, + "address": src_token, + "decimals": src_dec, + "amount": amount_wei, + }, + "tokenOut": { + "chainId": dst_chain, + "address": dst_token, + "decimals": dst_dec, + }, + "from": wallet, + "to": wallet, + "slippage": slippage, + "partnerId": PARTNER_ID, + }) + + if "tx" not in result: + msg = result.get("message", result.get("error", json.dumps(result))) + print(f"ERROR from Symbiosis API: {msg}", file=sys.stderr) + sys.exit(1) + + tx = result["tx"] + approve_to = result.get("approveTo", "") + out = result.get("tokenAmountOut", {}) + fee = result.get("fee", {}) + + out_human = format_units(out.get("amount", "0"), out.get("decimals", dst_dec)) + fee_human = float(format_units(fee.get("amount", "0"), fee.get("decimals", 6))) + fee_usd = fee_human * fee.get("priceUsd", 1) + est_time = result.get("estimatedTime", "?") + + print(f"Quote: {amount} -> {out_human}") + print(f"Fee: ~${fee_usd:.4f}") + print(f"Estimated time: {est_time}s") + print(f"Symbiosis router: {tx['to']}") + + # --- Step 2: Approve (if needed) --- + if approve_to and src_token.lower() != ZERO_ADDR: + print("\n=== Approving token for Symbiosis ===") + padded = approve_to[2:].lower().zfill(64) + approve_data = f"0x095ea7b3{padded}{MAX_UINT256}" + + approve_result = bankr_submit(bankr_key, { + "to": src_token, + "chainId": src_chain, + "value": "0", + "data": approve_data, + }, "Approve token for Symbiosis cross-chain swap") + + if not approve_result.get("success"): + print(f"ERROR: Approve failed: {json.dumps(approve_result)}", file=sys.stderr) + sys.exit(1) + print(f"Approve tx: {approve_result['transactionHash']}") + + # --- Step 3: Submit swap --- + print("\n=== Submitting Symbiosis swap ===") + swap_result = bankr_submit(bankr_key, { + "to": tx["to"], + "chainId": src_chain, + "value": tx.get("value", "0"), + "data": tx["data"], + }, "Symbiosis cross-chain swap") + + if not swap_result.get("success"): + print(f"ERROR: Swap failed: {json.dumps(swap_result)}", file=sys.stderr) + sys.exit(1) + + swap_hash = swap_result["transactionHash"] + print(f"Swap tx: {swap_hash}") + print(f"Status: {swap_result.get('status', 'unknown')}") + print(f"\n=== SUCCESS ===") + print(f"Swapped {amount} on chain {src_chain} -> ~{out_human} on chain {dst_chain}") + print(f"Fee: ~${fee_usd:.4f} | Estimated arrival: {est_time}s") + print(f"Track: https://explorer.symbiosis.finance/transactions/{src_chain}/{swap_hash}") + + +if __name__ == "__main__": + main()