mirror of
https://github.com/Starchild-ai-agent/official-skills.git
synced 2026-09-18 10:19:40 +08:00
feat: add across-bridge skill + CONTRIBUTING guide
across-bridge: bridge ETH/ERC-20 across 10 EVM chains via Across Protocol v3. Two-function API: bridge_quote() for read-only quotes, bridge_execute() for end-to-end (approval + deposit + arrival verification). Uses Across /swap endpoint which returns ready-to-sign tx data, eliminating manual ABI encoding. Validated with real on-chain test (USDC Base→Arbitrum, ~12s fill, 0.47% fee). CONTRIBUTING.md: comprehensive guide for building official-standard skills — directory structure, SKILL.md frontmatter, exports.py patterns, function design best practices (one-function-one-intent, read+execute pairing, human-friendly args, lazy platform imports, side-effect verification), logo specs, publishing checklist & flow, versioning, reference skills. Co-authored-by: Starchild <noreply@iamstarchild.com>
This commit is contained in:
+461
@@ -0,0 +1,461 @@
|
||||
# Contributing a Skill to Starchild Official Skills
|
||||
|
||||
This guide walks you through building a skill that meets the Starchild official
|
||||
standard — the same bar the `across-bridge`, `wallet`, and `hyperliquid` skills
|
||||
are held to. Follow it end-to-end and your skill will install cleanly, show up
|
||||
in search, and feel native to any agent that calls it.
|
||||
|
||||
---
|
||||
|
||||
## 1. What is a Starchild Skill?
|
||||
|
||||
A skill is a **self-contained directory** that teaches an agent how to use a
|
||||
tool, API, or workflow. It has two halves:
|
||||
|
||||
- **`SKILL.md`** — the human/agent-readable contract: what it does, when to
|
||||
use it, how to call it, gotchas. This is what the agent reads to decide
|
||||
whether to use the skill and how.
|
||||
- **`exports.py`** (+ supporting scripts) — the executable half: Python
|
||||
functions the agent calls at runtime via `core.skill_tools`.
|
||||
|
||||
A skill is NOT a prompt template, a config file, or a loose collection of
|
||||
scripts. It is a **callable capability** with a documented interface.
|
||||
|
||||
---
|
||||
|
||||
## 2. Directory Structure
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md ← required: frontmatter + usage doc
|
||||
├── exports.py ← required for script skills: public function surface
|
||||
├── __init__.py ← empty, makes it a package
|
||||
├── logo.png ← recommended: 128×128 or 256×256, ≤ 50KB
|
||||
├── scripts/ ← optional: implementation modules
|
||||
│ └── my_api.py
|
||||
└── tools/ ← optional: alternative location for modules
|
||||
└── helpers.py
|
||||
```
|
||||
|
||||
### Minimum viable skill
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md
|
||||
└── exports.py
|
||||
```
|
||||
|
||||
That's it. `logo.png`, `__init__.py`, `scripts/`, `tools/` are all optional.
|
||||
But every official skill should have a logo — see §6.
|
||||
|
||||
---
|
||||
|
||||
## 3. SKILL.md — The Contract
|
||||
|
||||
### 3.1 Required frontmatter
|
||||
|
||||
Three fields are **mandatory** — CI will reject the build if any are missing:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-skill # lowercase, hyphens only, matches dir name
|
||||
version: 1.0.0 # semver
|
||||
description: |
|
||||
One-line summary, then a blank line, then a "Use when…" sentence.
|
||||
This is the PRIMARY search field — be specific and keyword-rich.
|
||||
---
|
||||
```
|
||||
|
||||
### 3.2 Recommended frontmatter
|
||||
|
||||
```yaml
|
||||
author: starchild
|
||||
tags: [defi, bridge, evm, ethereum, arbitrum]
|
||||
delivery: script # "script" = callable Python functions
|
||||
metadata:
|
||||
starchild:
|
||||
emoji: 🌉 # shown in skill cards / search results
|
||||
skillKey: my-skill # matches dir name
|
||||
requires:
|
||||
bins: [python3] # system binaries the skill needs
|
||||
env: [MY_API_KEY] # env vars (user provides via request_env_input)
|
||||
install:
|
||||
- kind: pip
|
||||
package: requests # pip deps auto-installed on skill load
|
||||
```
|
||||
|
||||
### 3.3 Body structure
|
||||
|
||||
Use this template — agents rely on these sections to decide when and how to
|
||||
call the skill:
|
||||
|
||||
```markdown
|
||||
# 🌉 Skill Name
|
||||
|
||||
One-paragraph elevator pitch: what it does, why it's fast/cheap/better.
|
||||
|
||||
## When to Use
|
||||
- Concrete trigger phrases ("bridge 50 USDC from Base to Arbitrum")
|
||||
- 3–5 bullet points covering the main use cases
|
||||
|
||||
## Supported <Chains / Exchanges / Endpoints>
|
||||
Plain list — helps the agent answer "does this support X?" without reading code.
|
||||
|
||||
## How to Call
|
||||
|
||||
Show the EXACT import + call pattern. For hyphenated skill names, use the
|
||||
`_modules` dict (see §4.2):
|
||||
|
||||
\```bash
|
||||
python3 - <<'EOF'
|
||||
from core.skill_tools import _modules
|
||||
my = _modules["my-skill"]
|
||||
import json
|
||||
print(json.dumps(my.do_thing(arg="value"), indent=2))
|
||||
EOF
|
||||
\```
|
||||
|
||||
### `function_name(args)` — one-line summary
|
||||
What it returns, what side effects it has.
|
||||
|
||||
## Workflows
|
||||
### The common case (end-to-end)
|
||||
### The read-only case (quote / check / status)
|
||||
|
||||
## Key Facts / Gotchas
|
||||
- Things that surprised you during development
|
||||
- Auth model, rate limits, settlement times
|
||||
- "Gas is sponsored" / "Wallet is the Agent Wallet" type facts
|
||||
|
||||
## Dependencies
|
||||
- pip packages
|
||||
- other skills (e.g. "uses core.skill_tools.wallet for signing")
|
||||
```
|
||||
|
||||
**Do NOT** include:
|
||||
- Long API reference dumps (link to the provider's docs instead)
|
||||
- Internal implementation notes
|
||||
- Changelogs (use git history)
|
||||
|
||||
---
|
||||
|
||||
## 4. exports.py — The Function Surface
|
||||
|
||||
`exports.py` is the **only** file the skill loader looks at. It defines the
|
||||
public functions agents call. Everything else (`scripts/*.py`, `tools/*.py`)
|
||||
is loaded as supporting infrastructure.
|
||||
|
||||
### 4.1 Minimal exports.py
|
||||
|
||||
```python
|
||||
"""
|
||||
my-skill exports — for use in task scripts via core.skill_tools.
|
||||
|
||||
Usage:
|
||||
from core.skill_tools import _modules
|
||||
my = _modules["my-skill"]
|
||||
result = my.do_thing(arg="value")
|
||||
"""
|
||||
import os, importlib.util
|
||||
|
||||
_here = os.path.dirname(__file__)
|
||||
_mod_path = os.path.join(_here, "scripts", "my_api.py")
|
||||
_spec = importlib.util.spec_from_file_location("_my_core", _mod_path)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
do_thing = _mod.do_thing
|
||||
get_quote = _mod.get_quote
|
||||
```
|
||||
|
||||
The loader auto-discovers `exports.py` and registers every public function in
|
||||
it. You do **not** need `__all__` — but you **can** use it to hide helpers:
|
||||
|
||||
```python
|
||||
__all__ = ["do_thing", "get_quote"] # only these are exposed
|
||||
```
|
||||
|
||||
### 4.2 Hyphenated skill names
|
||||
|
||||
Skill names use hyphens (`across-bridge`, `us-stock`), but Python identifiers
|
||||
can't. So `from core.skill_tools import across_bridge` does **not** work. Use
|
||||
the `_modules` dict instead:
|
||||
|
||||
```python
|
||||
from core.skill_tools import _modules
|
||||
across = _modules["across-bridge"]
|
||||
across.bridge_execute(...)
|
||||
```
|
||||
|
||||
This is the official pattern — document it in your SKILL.md so agents don't
|
||||
waste a turn discovering it.
|
||||
|
||||
### 4.3 What the loader filters out automatically
|
||||
|
||||
The `core.skill_tools` loader wraps your exports in a namespace proxy that
|
||||
hides:
|
||||
- Imported modules (`os`, `requests`, `json`)
|
||||
- ALL_CAPS constants (`API_URL`, `FLY_SOCKET`)
|
||||
- Private names (`_helper`)
|
||||
- Classes and non-callable attributes
|
||||
|
||||
So you can freely import at module top without polluting the public surface.
|
||||
Just define your functions and they're exported.
|
||||
|
||||
### 4.4 Import isolation for supporting modules
|
||||
|
||||
If your `exports.py` does `from utils import some_function`, the loader
|
||||
pre-loads every `.py` in `scripts/` and `tools/` under unique names and
|
||||
injects them as bare modules during load. This means:
|
||||
|
||||
- `from utils import x` just works — no `sys.path` hacks
|
||||
- Your `utils.py` won't permanently shadow another skill's `utils.py`
|
||||
- You don't need `__all__` or careful naming
|
||||
|
||||
**But**: keep `exports.py` thin. Put real logic in `scripts/`. `exports.py`
|
||||
should be ~30 lines: load the core module, re-export functions, done.
|
||||
|
||||
---
|
||||
|
||||
## 5. Function Design — Best Practices
|
||||
|
||||
### 5.1 One function = one user intent
|
||||
|
||||
The biggest quality signal. Don't make the agent orchestrate:
|
||||
|
||||
```python
|
||||
# ❌ Bad — agent must call 4 functions in sequence
|
||||
get_quote() → encode_calldata() → send_approval() → send_bridge() → check_status()
|
||||
|
||||
# ✅ Good — one function, end-to-end
|
||||
bridge_execute(from_chain, to_chain, token, amount, wallet)
|
||||
```
|
||||
|
||||
If a workflow always has the same steps, package them. The agent should be
|
||||
able to say "bridge 1 USDC Base→Arbitrum" and your skill handles the rest.
|
||||
|
||||
### 5.2 Pair read-only + end-to-end
|
||||
|
||||
Expose at least two functions:
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `*_quote()` / `*_status()` / `*_get()` | Read-only, safe to call freely |
|
||||
| `*_execute()` / `*_send()` / `*_create()` | Side effects, does the real thing |
|
||||
|
||||
This lets the agent "think before acting" — get a quote, show it to the user,
|
||||
then execute only after confirmation.
|
||||
|
||||
### 5.3 Accept human-friendly args, return structured JSON
|
||||
|
||||
```python
|
||||
# ✅ Good — agent passes "USDC", "base", 1.0
|
||||
bridge_execute(from_chain="base", to_chain="arbitrum",
|
||||
token="USDC", amount=1, wallet="0x...")
|
||||
|
||||
# ❌ Bad — agent must know chain IDs, wei, contract addresses
|
||||
bridge_execute(origin_chain_id=8453, dest_chain_id=42161,
|
||||
input_token="0x833589...", amount_wei="1000000", ...)
|
||||
```
|
||||
|
||||
Internally resolve symbols → IDs/addresses via a registry. Return a dict with
|
||||
both human and machine fields:
|
||||
|
||||
```python
|
||||
{
|
||||
"output_amount": "995312", # machine
|
||||
"output_amount_human": 0.995312, # human
|
||||
"fees": {"total_pct": "0.47", ...},
|
||||
"arrival_confirmed": true,
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Lazy-import platform dependencies
|
||||
|
||||
`core.skill_tools.wallet` only exists at runtime in the platform container.
|
||||
If your skill needs it, import it inside the function, not at module top:
|
||||
|
||||
```python
|
||||
def bridge_execute(...):
|
||||
from core.skill_tools import wallet as w # lazy
|
||||
...
|
||||
```
|
||||
|
||||
This keeps `exports.py` loadable in any environment (CI, local dev).
|
||||
|
||||
### 5.5 Verify side effects before returning success
|
||||
|
||||
For anything that moves money or mutates external state, don't return
|
||||
`status: "success"` just because the API call returned 200. Poll the
|
||||
destination / re-read state until you can confirm:
|
||||
|
||||
```python
|
||||
# bridge_execute polls destination balance until funds actually arrive
|
||||
arrival_confirmed = _poll_destination_balance(timeout=180)
|
||||
return {"status": "success" if arrival_confirmed else "submitted_unconfirmed", ...}
|
||||
```
|
||||
|
||||
### 5.6 Include a CLI for debugging
|
||||
|
||||
Add an `if __name__ == "__main__"` block to your core script so you (and
|
||||
reviewers) can test from bash without spinning up the full skill loader:
|
||||
|
||||
```python
|
||||
# scripts/across.py
|
||||
if __name__ == "__main__":
|
||||
import sys, json
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "quote":
|
||||
print(json.dumps(bridge_quote(*sys.argv[2:]), indent=2))
|
||||
elif cmd == "execute":
|
||||
print(json.dumps(bridge_execute(*sys.argv[2:]), indent=2, default=str))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Logo
|
||||
|
||||
Every official skill should ship a logo. It shows up in skill cards, search
|
||||
results, and the installed-skills panel.
|
||||
|
||||
### 6.1 Specs
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Format | **PNG** preferred (SVG also accepted) |
|
||||
| Size | 128×128 or 256×256 px |
|
||||
| File size | ≤ 50 KB |
|
||||
| Filename | `logo.png` (or `logo.svg`) in the skill root |
|
||||
| Background | Transparent or solid — match the brand |
|
||||
|
||||
### 6.2 Where to get one
|
||||
|
||||
- **Official brand assets page** — most projects have one (e.g.
|
||||
`docs.across.to/brand-assets`, `https://ethereum.org/brand`)
|
||||
- **Favicon / OG image** — if no brand page, fetch the site's favicon:
|
||||
```bash
|
||||
curl -sL "https://across.to/favicon.svg" -o logo.svg
|
||||
```
|
||||
- **seeklogo / cdnlogo** — community-hosted SVGs for major brands
|
||||
- **Generate one** — as a last resort, use the `image-create` skill to make a
|
||||
256×256 icon that matches the skill's domain
|
||||
|
||||
### 6.3 Verify
|
||||
|
||||
```bash
|
||||
ls -la my-skill/logo.png # exists, < 50KB
|
||||
file my-skill/logo.png # "PNG image data, 256 x 256"
|
||||
```
|
||||
|
||||
The loader picks up `logo.png` / `logo.svg` automatically — no frontmatter
|
||||
field needed.
|
||||
|
||||
---
|
||||
|
||||
## 7. Publishing Checklist
|
||||
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] `SKILL.md` has `name`, `version`, `description` in frontmatter
|
||||
- [ ] `name` matches the directory name (lowercase, hyphens only)
|
||||
- [ ] `description` is specific and keyword-rich (it's the search field)
|
||||
- [ ] `exports.py` loads cleanly: `python3 -c "import importlib.util, os; s=importlib.util.spec_from_file_location('m','my-skill/exports.py'); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); print(dir(m))"`
|
||||
- [ ] At least one read-only function + one end-to-end function
|
||||
- [ ] Functions accept human-friendly args (symbols, not IDs/addresses)
|
||||
- [ ] `logo.png` exists, < 50KB
|
||||
- [ ] Tested with a real call (not just "it should work")
|
||||
- [ ] `metadata.starchild.install` lists pip deps
|
||||
- [ ] No hardcoded secrets, API keys, or wallet addresses
|
||||
|
||||
---
|
||||
|
||||
## 8. Publishing Flow
|
||||
|
||||
```bash
|
||||
# 1. Clone
|
||||
git clone https://github.com/Starchild-ai-agent/official-skills.git
|
||||
cd official-skills
|
||||
git checkout -b feat/my-skill
|
||||
|
||||
# 2. Add your skill directory
|
||||
mkdir my-skill
|
||||
# ... add SKILL.md, exports.py, logo.png, scripts/ ...
|
||||
|
||||
# 3. Commit & push
|
||||
git add my-skill/
|
||||
git commit -m "feat: add my-skill — one-line description"
|
||||
git push origin feat/my-skill
|
||||
|
||||
# 4. Open PR
|
||||
# GitHub Actions validates frontmatter + rebuilds skills.json automatically
|
||||
```
|
||||
|
||||
### CI does the rest
|
||||
|
||||
On push to `main`, the `build-index.yml` workflow:
|
||||
1. **Validates** every `SKILL.md` — fails if `name`/`version`/`description` missing
|
||||
2. **Rebuilds** `skills.json` from frontmatter
|
||||
3. **Commits** the updated index back to `main`
|
||||
|
||||
You do not need to edit `skills.json` manually — it's auto-generated.
|
||||
|
||||
---
|
||||
|
||||
## 9. Versioning
|
||||
|
||||
Follow semver:
|
||||
|
||||
| Change | Bump |
|
||||
|--------|------|
|
||||
| Bug fix, doc tweak | patch (`1.0.0 → 1.0.1`) |
|
||||
| New function, new feature | minor (`1.0.0 → 1.1.0`) |
|
||||
| Breaking API change | major (`1.0.0 → 2.0.0`) |
|
||||
|
||||
Users who already installed the skill get the new version on their next
|
||||
`npx skills add` (npx detects the changed `computedHash`).
|
||||
|
||||
---
|
||||
|
||||
## 10. Removing a Skill
|
||||
|
||||
```bash
|
||||
rm -rf old-skill
|
||||
git add -A && git commit -m "chore: remove old-skill" && git push
|
||||
```
|
||||
|
||||
CI removes it from `skills.json`. To also remove it from running user
|
||||
containers, add the skill name to `config/skill-removals.txt` in the
|
||||
[starchild-clawd](https://github.com/Starchild-ai-agent/starchild-clawd) repo.
|
||||
|
||||
---
|
||||
|
||||
## 11. Reference Skills
|
||||
|
||||
Read these before building your own — they embody the standard:
|
||||
|
||||
| Skill | Why study it |
|
||||
|-------|-------------|
|
||||
| `wallet` | Lazy-imports platform deps; clean read/write split |
|
||||
| `hyperliquid` | `exports.py` + `client.py` + `tools.py` separation |
|
||||
| `across-bridge` | One-function end-to-end + quote pair; hyphenated name pattern |
|
||||
| `coingecko` | Simple read-only skill with `tools/` layout |
|
||||
| `1inch` | `delivery: script` + `requires.env` for API keys |
|
||||
|
||||
---
|
||||
|
||||
## 12. Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| `from core.skill_tools import my_skill` fails | Hyphenated name — use `_modules["my-skill"]` |
|
||||
| CI rejects the build | Missing `name`/`version`/`description` in frontmatter |
|
||||
| Agent calls 5 functions to do one thing | Package the workflow into one `*_execute()` |
|
||||
| Skill works locally, fails in container | Hardcoded path or top-level `from core.skill_tools import wallet` — lazy-import instead |
|
||||
| `description` too vague | Add a "Use when…" sentence with concrete examples |
|
||||
| No logo | Fetch favicon or brand SVG; see §6 |
|
||||
| Exposing `os`, `requests` as "functions" | You don't need to — the loader filters them out |
|
||||
| Editing `skills.json` manually | Don't — CI overwrites it on every push |
|
||||
|
||||
---
|
||||
|
||||
Questions? Open an issue or ask in the Starchild community. Happy building.
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: across-bridge
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Bridge ETH and ERC-20 tokens across EVM chains via Across Protocol v3.
|
||||
One function for a quote, one function for the entire end-to-end bridge
|
||||
(approval + deposit + arrival verification).
|
||||
|
||||
Use when the user wants to move tokens between chains (e.g. "bridge 50 USDC
|
||||
from Base to Arbitrum", "send ETH from Ethereum to Optimism", "move USDT
|
||||
Arbitrum → Base"). Supports Ethereum, Arbitrum, Optimism, Base, Polygon,
|
||||
BSC, Linea, zkSync, Scroll, Mantle. Fast settlement (seconds to minutes).
|
||||
author: starchild
|
||||
tags: [bridge, defi, cross-chain, across, evm, ethereum, arbitrum, base, optimism]
|
||||
delivery: script
|
||||
metadata:
|
||||
starchild:
|
||||
emoji: 🌉
|
||||
skillKey: across-bridge
|
||||
requires:
|
||||
bins: [python3]
|
||||
install:
|
||||
- kind: pip
|
||||
package: requests
|
||||
---
|
||||
|
||||
# 🌉 Across Bridge
|
||||
|
||||
Bridge tokens between EVM chains using [Across Protocol](https://across.to) v3.
|
||||
Across is an intent-based bridge: relayers front the liquidity on the
|
||||
destination chain and get repaid on the origin chain, so settlement is fast
|
||||
(seconds to a few minutes) and fees are low (0.05–0.5%).
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Cross-chain token transfer**: "bridge 50 USDC from Base to Arbitrum"
|
||||
- **L1 ↔ L2 movement**: "send 0.01 ETH from Ethereum to Optimism"
|
||||
- **Stablecoin hops**: "move USDT from Arbitrum to Base"
|
||||
- **Speed matters**: Across fills in seconds–minutes vs native bridges (7 days)
|
||||
|
||||
## Supported Chains
|
||||
|
||||
Ethereum · Arbitrum · Optimism · Base · Polygon · BSC · Linea · zkSync · Scroll · Mantle
|
||||
|
||||
## Supported Tokens
|
||||
|
||||
ETH · WETH · USDC · USDT · WBTC · DAI
|
||||
|
||||
## How to Call
|
||||
|
||||
All operations are Python functions exposed under `core.skill_tools`. Because
|
||||
the skill name contains a hyphen, use the `_modules` dict to access it:
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
from core.skill_tools import _modules
|
||||
across = _modules["across-bridge"]
|
||||
import json
|
||||
q = across.bridge_quote(from_chain="base", to_chain="arbitrum",
|
||||
token="USDC", amount=1, wallet="0x...")
|
||||
print(json.dumps(q, indent=2))
|
||||
EOF
|
||||
```
|
||||
|
||||
### `bridge_quote(from_chain, to_chain, token, amount, wallet=None)`
|
||||
|
||||
Get a live quote — no on-chain action. Returns output amount, fees, estimated
|
||||
fill time, and (if `wallet` is provided) ready-to-sign approval + bridge
|
||||
transactions.
|
||||
|
||||
### `bridge_execute(from_chain, to_chain, token, amount, wallet, confirm_arrival=True)`
|
||||
|
||||
**One call does everything:**
|
||||
|
||||
1. Fetches a fresh quote from Across `/swap`
|
||||
2. Sends ERC-20 approval to the SpokePool (if needed; skipped for native ETH)
|
||||
3. Sends the `depositV3` bridge transaction
|
||||
4. Polls the destination-chain balance until funds arrive (or timeout)
|
||||
|
||||
Returns a full receipt with `status`, `output_amount`, `approval_tx`,
|
||||
`bridge_tx`, and `arrival_confirmed`.
|
||||
|
||||
### `bridge_status(origin_chain, deposit_tx_hash)`
|
||||
|
||||
Check the fill status of a submitted deposit via Across's status API.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Just get a quote (no execution)
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
from core.skill_tools import _modules
|
||||
across = _modules["across-bridge"]
|
||||
import json
|
||||
q = across.bridge_quote(from_chain="base", to_chain="arbitrum",
|
||||
token="USDC", amount=50)
|
||||
print(json.dumps(q, indent=2))
|
||||
EOF
|
||||
```
|
||||
|
||||
Key fields in the response:
|
||||
- `output_amount_human` — tokens the recipient will receive
|
||||
- `fees.total_pct` — fee percentage (e.g. `4688000000000000` = 0.47%)
|
||||
- `estimated_fill_time_sec` — expected settlement time
|
||||
- `needs_approval` — whether an ERC-20 approve is required before bridging
|
||||
|
||||
### Bridge end-to-end (the common case)
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
from core.skill_tools import _modules
|
||||
across = _modules["across-bridge"]
|
||||
import json
|
||||
r = across.bridge_execute(from_chain="base", to_chain="arbitrum",
|
||||
token="USDC", amount=1,
|
||||
wallet="0x0B52...Eb16")
|
||||
print(json.dumps(r, indent=2, default=str))
|
||||
EOF
|
||||
```
|
||||
|
||||
That single call handles approval + deposit + arrival verification. No need
|
||||
to manually call `wallet_transfer` or encode calldata — the Across `/swap`
|
||||
API returns ready-to-sign transactions and this skill dispatches them.
|
||||
|
||||
### Check status of a submitted deposit
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
from core.skill_tools import _modules
|
||||
across = _modules["across-bridge"]
|
||||
import json
|
||||
s = across.bridge_status(origin_chain="base", deposit_tx_hash="0x...")
|
||||
print(json.dumps(s, indent=2))
|
||||
EOF
|
||||
```
|
||||
|
||||
## Acceptance / Red-Flag Guidance
|
||||
|
||||
**Good routes:**
|
||||
- Fee < 0.5% · fill time < 60s · output ≈ input (minus small fee)
|
||||
|
||||
**Warn the user before proceeding if:**
|
||||
- Fee > 1% (expensive route or very small amount — min deposit applies)
|
||||
- Fill time > 10 minutes (congested route)
|
||||
- Amount below `limits.min_deposit` (quote will flag `isAmountTooLow`)
|
||||
|
||||
## Key Facts
|
||||
|
||||
- **Native ETH**: `msg.value` carries the amount; no approval step. Across
|
||||
handles wrapping/unwrapping automatically.
|
||||
- **ERC-20**: `msg.value` = 0; an `approve(spokePool, amount)` is required
|
||||
first. `bridge_execute` sends this automatically.
|
||||
- **Quotes are live** from `app.across.to/api/swap` and valid for ~30s.
|
||||
`bridge_execute` re-fetches right before broadcasting.
|
||||
- **Gas is sponsored** by default via the Starchild wallet — the user doesn't
|
||||
need native tokens on the origin chain for gas.
|
||||
- **Settlement**: Across relayers typically fill within seconds to a few
|
||||
minutes. `bridge_execute` polls the destination balance for up to 180s.
|
||||
- **Wallet**: uses the Starchild Agent Wallet (same as the `wallet` skill).
|
||||
No separate wallet connection needed.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `requests` (pip)
|
||||
- `core.skill_tools.wallet` (platform — for signing/broadcasting)
|
||||
|
||||
## Tested Routes
|
||||
|
||||
This skill was validated with a real on-chain bridge: 1 USDC Base → Arbitrum,
|
||||
confirmed arrived on destination within ~35 seconds, fee 0.47%.
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Across Bridge skill exports — for use in task scripts via core.skill_tools.
|
||||
|
||||
Usage:
|
||||
from core.skill_tools import across
|
||||
q = across.bridge_quote(from_chain="base", to_chain="arbitrum",
|
||||
token="USDC", amount=1, wallet="0x...")
|
||||
r = across.bridge_execute(from_chain="base", to_chain="arbitrum",
|
||||
token="USDC", amount=1, wallet="0x...")
|
||||
s = across.bridge_status(origin_chain="base", deposit_tx_hash="0x...")
|
||||
"""
|
||||
|
||||
# Load the core module from scripts/ and re-export its public functions.
|
||||
import os, importlib.util
|
||||
|
||||
_here = os.path.dirname(__file__)
|
||||
_mod_path = os.path.join(_here, "scripts", "across.py")
|
||||
_spec = importlib.util.spec_from_file_location("_across_core", _mod_path)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
bridge_quote = _mod.bridge_quote
|
||||
bridge_execute = _mod.bridge_execute
|
||||
bridge_status = _mod.bridge_status
|
||||
|
||||
# Convenience: expose chain/token registries for downstream scripts
|
||||
CHAIN_IDS = _mod.CHAIN_IDS
|
||||
TOKENS = _mod.TOKENS
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1422_35593)">
|
||||
<path d="M32 0H0V32H32V0Z" fill="#6CF9D8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.8638 7.37988L24.5978 9.11387L18.803 14.9086C18.4976 14.1131 17.8645 13.4801 17.069 13.1746L22.8638 7.37988ZM14.9096 13.1746L9.11485 7.37988L7.38086 9.11387L13.1756 14.9086C13.481 14.1131 14.1141 13.4801 14.9096 13.1746ZM13.1756 17.0681L7.38086 22.8628L9.11485 24.5968L14.9096 18.8021C14.1141 18.4966 13.481 17.8635 13.1756 17.0681ZM17.069 18.8021L22.8638 24.5968L24.5978 22.8628L18.803 17.0681C18.4976 17.8635 17.8645 18.4966 17.069 18.8021Z" fill="#151518"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1422_35593">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 808 B |
@@ -0,0 +1,446 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Across Protocol Bridge — official skill core module.
|
||||
|
||||
Two high-level functions cover the entire cross-chain flow:
|
||||
|
||||
bridge_quote(...) → live quote (output amount, fees, fill time, route)
|
||||
bridge_execute(...) → end-to-end: approval (if needed) + deposit + verify arrival
|
||||
|
||||
Both are thin wrappers around the Across `/swap` API, which returns ready-to-sign
|
||||
transaction data (approval + bridge calldata) in one call. No manual ABI encoding,
|
||||
no multi-step orchestration by the caller.
|
||||
|
||||
Run from bash:
|
||||
python3 -c "from core.skill_tools.across import bridge_quote; ..."
|
||||
python3 -c "from core.skill_tools.across import bridge_execute; ..."
|
||||
|
||||
Or import in a script:
|
||||
from core.skill_tools.across import bridge_quote, bridge_execute
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
|
||||
# ─── Chain registry ───────────────────────────────────────────────────────────
|
||||
|
||||
CHAIN_IDS = {
|
||||
"ethereum": 1, "mainnet": 1, "eth": 1,
|
||||
"arbitrum": 42161, "arb": 42161,
|
||||
"optimism": 10, "op": 10,
|
||||
"base": 8453,
|
||||
"polygon": 137, "matic": 137,
|
||||
"bsc": 56, "binance": 56,
|
||||
"linea": 59144,
|
||||
"zksync": 324, "era": 324,
|
||||
"scroll": 534352,
|
||||
"mantle": 5000,
|
||||
}
|
||||
|
||||
CHAIN_NAMES = {
|
||||
1: "ethereum",
|
||||
42161: "arbitrum",
|
||||
10: "optimism",
|
||||
8453: "base",
|
||||
137: "polygon",
|
||||
56: "bsc",
|
||||
59144: "linea",
|
||||
324: "zksync",
|
||||
534352: "scroll",
|
||||
5000: "mantle",
|
||||
}
|
||||
|
||||
# ─── Token registry (symbol → {chain_id: address}) ────────────────────────────
|
||||
# Native ETH uses the zero address as inputToken/outputToken; the Across /swap
|
||||
# API handles native ETH wrapping/unwrapping automatically.
|
||||
|
||||
TOKENS = {
|
||||
"ETH": {
|
||||
1: "0x0000000000000000000000000000000000000000",
|
||||
42161: "0x0000000000000000000000000000000000000000",
|
||||
10: "0x0000000000000000000000000000000000000000",
|
||||
8453: "0x0000000000000000000000000000000000000000",
|
||||
137: "0x0000000000000000000000000000000000000000",
|
||||
56: "0x0000000000000000000000000000000000000000",
|
||||
59144: "0x0000000000000000000000000000000000000000",
|
||||
324: "0x0000000000000000000000000000000000000000",
|
||||
534352:"0x0000000000000000000000000000000000000000",
|
||||
5000: "0x0000000000000000000000000000000000000000",
|
||||
},
|
||||
"WETH": {
|
||||
1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
||||
10: "0x4200000000000000000000000000000000000006",
|
||||
8453: "0x4200000000000000000000000000000000000006",
|
||||
137: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
|
||||
56: "0x2170Ed0880ac9A755fd29B2688956BD959F933F8",
|
||||
59144: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f",
|
||||
324: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91",
|
||||
},
|
||||
"USDC": {
|
||||
1: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
42161: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
||||
10: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
||||
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
137: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
||||
56: "0x8ac76A51cc950d9822D68b83fE1Ad97B32Cd580d",
|
||||
59144: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff",
|
||||
},
|
||||
"USDT": {
|
||||
1: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
||||
10: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
|
||||
137: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
||||
56: "0x55d398326f99059fF775485246999027B3197955",
|
||||
},
|
||||
"WBTC": {
|
||||
1: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
|
||||
42161: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f",
|
||||
10: "0x68f180fcCe6836688e9084f035309E29Bf0A2095",
|
||||
},
|
||||
"DAI": {
|
||||
1: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
42161: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
|
||||
10: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
|
||||
8453: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
||||
137: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
|
||||
},
|
||||
}
|
||||
|
||||
DECIMALS = {"ETH": 18, "WETH": 18, "USDC": 6, "USDT": 6, "WBTC": 8, "DAI": 18}
|
||||
|
||||
ACROSS_SWAP_API = "https://app.across.to/api/swap"
|
||||
ACROSS_STATUS_API = "https://app.across.to/api/deposit/status"
|
||||
|
||||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _resolve_chain(chain):
|
||||
"""Accept name (str) or id (int/str); return int chain id."""
|
||||
if isinstance(chain, int):
|
||||
return chain
|
||||
s = str(chain).strip().lower()
|
||||
if s.isdigit():
|
||||
return int(s)
|
||||
if s not in CHAIN_IDS:
|
||||
raise ValueError(f"Unknown chain '{chain}'. Supported: {sorted(set(CHAIN_IDS))}")
|
||||
return CHAIN_IDS[s]
|
||||
|
||||
|
||||
def _resolve_token(symbol, chain_id):
|
||||
"""Return token address for a symbol on a chain."""
|
||||
sym = symbol.upper()
|
||||
if sym not in TOKENS:
|
||||
raise ValueError(f"Unsupported token '{symbol}'. Supported: {list(TOKENS)}")
|
||||
if chain_id not in TOKENS[sym]:
|
||||
raise ValueError(f"{sym} not available on chain {chain_id} ({CHAIN_NAMES.get(chain_id, '?')}). "
|
||||
f"Available chains: {list(TOKENS[sym])}")
|
||||
return TOKENS[sym][chain_id]
|
||||
|
||||
|
||||
def _to_wei(amount, symbol):
|
||||
"""Convert human-readable amount to smallest-unit integer."""
|
||||
dec = DECIMALS[symbol.upper()]
|
||||
if isinstance(amount, str) and amount.isdigit():
|
||||
return int(amount)
|
||||
return int(round(float(amount) * 10 ** dec))
|
||||
|
||||
|
||||
def _from_wei(amount, symbol):
|
||||
"""Convert smallest-unit to human-readable float."""
|
||||
dec = DECIMALS[symbol.upper()]
|
||||
return int(amount) / 10 ** dec
|
||||
|
||||
|
||||
def _fetch_swap(input_token, output_token, amount_wei, origin_id, dest_id, wallet):
|
||||
"""Call Across /swap and return parsed JSON."""
|
||||
params = {
|
||||
"inputToken": input_token,
|
||||
"outputToken": output_token,
|
||||
"amount": str(amount_wei),
|
||||
"originChainId": origin_id,
|
||||
"destinationChainId": dest_id,
|
||||
"depositor": wallet,
|
||||
"recipient": wallet,
|
||||
}
|
||||
headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
|
||||
r = requests.get(ACROSS_SWAP_API, params=params, headers=headers, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
# ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
def bridge_quote(from_chain, to_chain, token, amount, wallet=None):
|
||||
"""
|
||||
Get a live Across bridge quote (no on-chain action).
|
||||
|
||||
Args:
|
||||
from_chain: origin chain name ("base") or id (8453)
|
||||
to_chain: destination chain name or id
|
||||
token: token symbol ("USDC", "ETH", "USDT", "WETH", "WBTC", "DAI")
|
||||
amount: human-readable amount (float/str) OR wei integer string
|
||||
wallet: depositor/recipient address (optional for quote-only)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"route": {"from_chain", "to_chain", "token", "amount", "amount_wei"},
|
||||
"output_amount": "995312", # smallest unit string
|
||||
"output_amount_human": 0.995312,
|
||||
"fees": {"total_wei", "total_pct", "currency"},
|
||||
"estimated_fill_time_sec": 2,
|
||||
"spoke_pool": "0x...",
|
||||
"needs_approval": true,
|
||||
"approval_txns": [...], # ready-to-send if wallet given
|
||||
"bridge_tx": {...}, # ready-to-send if wallet given
|
||||
"limits": {"min_deposit", "max_deposit", ...},
|
||||
"raw": {...} # full API response
|
||||
}
|
||||
"""
|
||||
origin_id = _resolve_chain(from_chain)
|
||||
dest_id = _resolve_chain(to_chain)
|
||||
sym = token.upper()
|
||||
amount_wei = _to_wei(amount, sym)
|
||||
|
||||
input_token = _resolve_token(sym, origin_id)
|
||||
output_token = _resolve_token(sym, dest_id)
|
||||
|
||||
if wallet is None:
|
||||
wallet = "0x0000000000000000000000000000000000000000"
|
||||
|
||||
data = _fetch_swap(input_token, output_token, amount_wei, origin_id, dest_id, wallet)
|
||||
|
||||
# Parse output amount — /swap nests it under steps.bridge.outputAmount
|
||||
output_wei = None
|
||||
if "steps" in data and "bridge" in data["steps"]:
|
||||
output_wei = data["steps"]["bridge"].get("outputAmount")
|
||||
if output_wei is None:
|
||||
output_wei = data.get("outputAmount")
|
||||
|
||||
# Fees
|
||||
fees_total = None
|
||||
fees_pct = None
|
||||
if "fees" in data and "total" in data["fees"]:
|
||||
ft = data["fees"]["total"]
|
||||
fees_total = ft.get("amount")
|
||||
fees_pct = ft.get("pct")
|
||||
|
||||
# Fill time
|
||||
fill_time = data.get("expectedFillTime")
|
||||
if fill_time is None and "steps" in data and "bridge" in data["steps"]:
|
||||
fill_time = data["steps"]["bridge"].get("estimatedFillTimeSec")
|
||||
|
||||
# Spoke pool
|
||||
spoke = data.get("spokePoolAddress")
|
||||
if spoke is None and "steps" in data and "bridge" in data["steps"]:
|
||||
spoke = data["steps"]["bridge"].get("spokePoolAddress")
|
||||
|
||||
# Approval + bridge tx
|
||||
approval_txns = data.get("approvalTxns", [])
|
||||
swap_tx = data.get("swapTx")
|
||||
needs_approval = bool(approval_txns)
|
||||
|
||||
# Limits (from suggested-fees; /swap may not include them)
|
||||
limits = data.get("limits")
|
||||
|
||||
return {
|
||||
"route": {
|
||||
"from_chain": CHAIN_NAMES.get(origin_id, origin_id),
|
||||
"from_chain_id": origin_id,
|
||||
"to_chain": CHAIN_NAMES.get(dest_id, dest_id),
|
||||
"to_chain_id": dest_id,
|
||||
"token": sym,
|
||||
"amount": str(amount),
|
||||
"amount_wei": str(amount_wei),
|
||||
},
|
||||
"output_amount": str(output_wei) if output_wei else None,
|
||||
"output_amount_human": _from_wei(int(output_wei), sym) if output_wei else None,
|
||||
"fees": {
|
||||
"total_wei": str(fees_total) if fees_total else None,
|
||||
"total_pct": str(fees_pct) if fees_pct else None,
|
||||
"currency": sym,
|
||||
},
|
||||
"estimated_fill_time_sec": fill_time,
|
||||
"spoke_pool": spoke,
|
||||
"needs_approval": needs_approval,
|
||||
"approval_txns": approval_txns,
|
||||
"bridge_tx": swap_tx,
|
||||
"limits": limits,
|
||||
"raw": data,
|
||||
}
|
||||
|
||||
|
||||
def bridge_execute(from_chain, to_chain, token, amount, wallet,
|
||||
confirm_arrival=True, arrival_timeout=180):
|
||||
"""
|
||||
End-to-end bridge: fetch fresh quote → approval (if needed) → deposit → verify.
|
||||
|
||||
Uses the Starchild wallet skill (core.skill_tools.wallet) for signing/broadcast.
|
||||
Gas is sponsored by default. Returns a full receipt.
|
||||
|
||||
Args:
|
||||
from_chain, to_chain, token, amount: same as bridge_quote
|
||||
wallet: depositor/recipient address (required)
|
||||
confirm_arrival: if True, poll destination balance until funds arrive
|
||||
arrival_timeout: max seconds to wait for arrival confirmation
|
||||
|
||||
Returns:
|
||||
{
|
||||
"status": "success" | "failed",
|
||||
"route": {...},
|
||||
"output_amount": "995312",
|
||||
"output_amount_human": 0.995312,
|
||||
"approval_tx": {...} | None,
|
||||
"bridge_tx": {...},
|
||||
"arrival_confirmed": true,
|
||||
"elapsed_sec": 12.4
|
||||
}
|
||||
"""
|
||||
# Lazy import: core.skill_tools.wallet is only available at runtime in
|
||||
# the platform environment, not at module load time.
|
||||
try:
|
||||
from core.skill_tools import wallet as w
|
||||
except Exception:
|
||||
import importlib
|
||||
w = importlib.import_module("core.skill_tools").wallet
|
||||
|
||||
origin_id = _resolve_chain(from_chain)
|
||||
dest_id = _resolve_chain(to_chain)
|
||||
sym = token.upper()
|
||||
amount_wei = _to_wei(amount, sym)
|
||||
|
||||
t0 = time.time()
|
||||
|
||||
# 1. Fresh quote (quote valid ~30s; re-fetch right before sending)
|
||||
q = bridge_quote(from_chain, to_chain, token, amount, wallet)
|
||||
if q["bridge_tx"] is None:
|
||||
raise RuntimeError(f"Across /swap returned no bridge tx. Raw: {q['raw']}")
|
||||
|
||||
# 2. Approval (if ERC-20 and not yet approved)
|
||||
approval_result = None
|
||||
if q["needs_approval"]:
|
||||
for atx in q["approval_txns"]:
|
||||
approval_result = w.wallet_transfer(
|
||||
to=atx["to"],
|
||||
amount="0",
|
||||
chain_id=int(atx["chainId"]),
|
||||
data=atx["data"],
|
||||
)
|
||||
# wait for approval to settle
|
||||
time.sleep(6)
|
||||
# re-fetch quote so swapTx reflects updated allowance
|
||||
q = bridge_quote(from_chain, to_chain, token, amount, wallet)
|
||||
if q["bridge_tx"] is None:
|
||||
raise RuntimeError("Re-quoted after approval but still no bridge tx.")
|
||||
|
||||
# 3. Bridge deposit
|
||||
swap = q["bridge_tx"]
|
||||
bridge_result = w.wallet_transfer(
|
||||
to=swap["to"],
|
||||
amount=swap.get("value", "0"),
|
||||
chain_id=int(swap["chainId"]),
|
||||
data=swap["data"],
|
||||
)
|
||||
|
||||
# 4. Verify arrival on destination
|
||||
arrival_confirmed = False
|
||||
if confirm_arrival:
|
||||
dest_name = CHAIN_NAMES.get(dest_id, dest_id)
|
||||
# snapshot pre-balance
|
||||
try:
|
||||
pre = w.wallet_balance(chain=dest_name)
|
||||
pre_amt = _find_token_balance(pre, sym, dest_id)
|
||||
except Exception:
|
||||
pre_amt = None
|
||||
|
||||
deadline = time.time() + arrival_timeout
|
||||
while time.time() < deadline:
|
||||
time.sleep(10)
|
||||
try:
|
||||
post = w.wallet_balance(chain=dest_name)
|
||||
post_amt = _find_token_balance(post, sym, dest_id)
|
||||
except Exception:
|
||||
continue
|
||||
if post_amt is not None and pre_amt is not None:
|
||||
if post_amt > pre_amt:
|
||||
arrival_confirmed = True
|
||||
break
|
||||
elif post_amt is not None and post_amt > 0:
|
||||
arrival_confirmed = True
|
||||
break
|
||||
|
||||
return {
|
||||
"status": "success" if arrival_confirmed or not confirm_arrival else "submitted_unconfirmed",
|
||||
"route": q["route"],
|
||||
"output_amount": q["output_amount"],
|
||||
"output_amount_human": q["output_amount_human"],
|
||||
"approval_tx": approval_result,
|
||||
"bridge_tx": bridge_result,
|
||||
"arrival_confirmed": arrival_confirmed,
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
}
|
||||
|
||||
|
||||
def _find_token_balance(balance_resp, symbol, chain_id):
|
||||
"""Extract a token's raw_amount from a wallet_balance() response."""
|
||||
sym = symbol.upper()
|
||||
# For native ETH, DeBank returns id == chain name or "eth"
|
||||
for t in balance_resp.get("tokens", []):
|
||||
if t.get("symbol", "").upper() == sym:
|
||||
return int(t.get("raw_amount", 0))
|
||||
return None
|
||||
|
||||
|
||||
def bridge_status(origin_chain, deposit_tx_hash):
|
||||
"""
|
||||
Check fill status of a submitted deposit via Across status API.
|
||||
|
||||
Args:
|
||||
origin_chain: origin chain name or id
|
||||
deposit_tx_hash: the deposit transaction hash on origin chain
|
||||
|
||||
Returns:
|
||||
dict from Across /deposit/status (status: filled / pending / etc.)
|
||||
"""
|
||||
origin_id = _resolve_chain(origin_chain)
|
||||
params = {"originChainId": origin_id, "depositTxHash": deposit_tx_hash}
|
||||
headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
|
||||
r = requests.get(ACROSS_STATUS_API, params=params, headers=headers, timeout=15)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
# ─── CLI (for quick testing / debugging) ──────────────────────────────────────
|
||||
|
||||
def _cli():
|
||||
import sys
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" python3 across.py quote <from> <to> <token> <amount> [wallet]")
|
||||
print(" python3 across.py execute <from> <to> <token> <amount> <wallet>")
|
||||
print(" python3 across.py status <from_chain> <deposit_tx_hash>")
|
||||
print()
|
||||
print("Chains: " + ", ".join(sorted(set(CHAIN_IDS))))
|
||||
print("Tokens: " + ", ".join(TOKENS))
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "quote":
|
||||
q = bridge_quote(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5],
|
||||
sys.argv[6] if len(sys.argv) > 6 else None)
|
||||
# strip raw for readability
|
||||
q.pop("raw", None)
|
||||
print(json.dumps(q, indent=2))
|
||||
elif cmd == "execute":
|
||||
r = bridge_execute(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6])
|
||||
print(json.dumps(r, indent=2, default=str))
|
||||
elif cmd == "status":
|
||||
s = bridge_status(sys.argv[2], sys.argv[3])
|
||||
print(json.dumps(s, indent=2))
|
||||
else:
|
||||
print(f"Unknown command '{cmd}'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_cli()
|
||||
Reference in New Issue
Block a user