@dev.sail.money/sailor 0.0.2-12 → 0.0.2-13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +27 -26
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +20 -40
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/ui/dist/assets/{add-DaJhwIBV.js → add-BHtIsGoW.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-BUxsqWXi.js → all-wallets-BH8IiQp_.js} +1 -1
- package/packages/ui/dist/assets/{app-store-DkltwTqE.js → app-store-C7je0Hvt.js} +1 -1
- package/packages/ui/dist/assets/{apple-owVOeaIT.js → apple-C24YGi7n.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-D2mmNJve.js → arrow-bottom-NmB75e6T.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-CbNYijx-.js → arrow-bottom-circle-D9vkbcGm.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-DJB61s4C.js → arrow-left-BXX7egmu.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-BBrsQ9R4.js → arrow-right-CL5l1ER5.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-Cil6bOc8.js → arrow-top-B3NcsHvl.js} +1 -1
- package/packages/ui/dist/assets/{bank-CbwEmRo3.js → bank-DyxCAL_C.js} +1 -1
- package/packages/ui/dist/assets/{basic-CLNfjw3m.js → basic-CnjXr1WW.js} +1 -1
- package/packages/ui/dist/assets/{browser-B5TtF4Pb.js → browser-C1sQ7PjQ.js} +1 -1
- package/packages/ui/dist/assets/{card-CO7BVB-C.js → card-DUcWbZgm.js} +1 -1
- package/packages/ui/dist/assets/{ccip-2W7K3_J3.js → ccip-ZC8164Ut.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-BEtSHq9m.js → checkmark-BFIb0gZI.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-D9xGHzPE.js → checkmark-bold-GxOovLvw.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-BDztht6i.js → chevron-bottom-BWFm5iOO.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-EV4GFNbc.js → chevron-left-Dw1vrfPS.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-B4_bB9oR.js → chevron-right-Lj-4a_f7.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-D54xPNzF.js → chevron-top-CNtyUs8e.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-DYUpAJJq.js → chrome-store-BFjfxT2g.js} +1 -1
- package/packages/ui/dist/assets/{clock-Ca1T1Soz.js → clock-upNWT1cC.js} +1 -1
- package/packages/ui/dist/assets/{close-BZqWjurK.js → close-BXOvqFtp.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-e6fl2XDo.js → coinPlaceholder-IDofua41.js} +1 -1
- package/packages/ui/dist/assets/{compass-DCLC7zIh.js → compass-Du2xGTuZ.js} +1 -1
- package/packages/ui/dist/assets/{copy-Th2AaD-O.js → copy-thfTJs3L.js} +1 -1
- package/packages/ui/dist/assets/{core-Ckx_cyuH.js → core-C8jSlwHY.js} +3 -3
- package/packages/ui/dist/assets/cursor-Dc-Hxk5X.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-BKHeABKB.js → cursor-transparent-DGzNzXxd.js} +1 -1
- package/packages/ui/dist/assets/{desktop-CBjY8t6F.js → desktop-BozY9d6T.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-DbSs2cli.js → disconnect-B-oC8Fzu.js} +1 -1
- package/packages/ui/dist/assets/{discord-ZlLOAUkM.js → discord-CpgvcuGY.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-CKUrqWYN.js → etherscan-DB-rL796.js} +1 -1
- package/packages/ui/dist/assets/{events-CiKP71cK.js → events-i9ztcj9W.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-DA1QzFiO.js → exclamation-triangle-Ct0mzv3Y.js} +1 -1
- package/packages/ui/dist/assets/{extension-BVJkmvpJ.js → extension-CubWAVae.js} +1 -1
- package/packages/ui/dist/assets/{external-link-D_bsR7B2.js → external-link-CgYSHh7f.js} +1 -1
- package/packages/ui/dist/assets/{facebook-CmFmhojx.js → facebook-DI_kGikU.js} +1 -1
- package/packages/ui/dist/assets/{fallback-Ofl6uSnB.js → fallback-CtKplO-p.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-Co-M3Ss8.js → farcaster-PEZAXYqi.js} +1 -1
- package/packages/ui/dist/assets/{filters-B1WwNaFU.js → filters-D6NPvINV.js} +1 -1
- package/packages/ui/dist/assets/{github-CP4fP6gn.js → github-BRk_ww3A.js} +1 -1
- package/packages/ui/dist/assets/{google-CsOIXJ6V.js → google-3tO0AiTd.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-DiMkomdF.js → help-circle-DtbseC_9.js} +1 -1
- package/packages/ui/dist/assets/{id-lmscL5LX.js → id-DNQF-HQS.js} +1 -1
- package/packages/ui/dist/assets/{image-B-ubJrY5.js → image-D4l2W9gw.js} +1 -1
- package/packages/ui/dist/assets/{index-DVgfCzCo.js → index-A8Mpr5Rm.js} +1 -1
- package/packages/ui/dist/assets/{index-BaukYv-x.js → index-BnYYo2S0.js} +1 -1
- package/packages/ui/dist/assets/{index-CZR1Qjhs.js → index-D-PqJKnq.js} +1 -1
- package/packages/ui/dist/assets/{index-Q2Yai4Fe.js → index-L6zhWbjO.js} +70 -70
- package/packages/ui/dist/assets/{index-Dbh5V1Z0.js → index-cl7Zazmz.js} +1 -1
- package/packages/ui/dist/assets/{index-CF0KMmke.js → index-vIXQ5sb2.js} +3 -3
- package/packages/ui/dist/assets/{index.es-C78cE5SI.js → index.es-BxPfiRyX.js} +4 -4
- package/packages/ui/dist/assets/{info-Cqg57EVo.js → info-DAwJNl3r.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-DkeSWNKV.js → info-circle-lvyXhzfD.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-DNlO4qKh.js → lightbulb-BkrBfCQo.js} +1 -1
- package/packages/ui/dist/assets/{mail-kVQ8Jb9Y.js → mail-CrRh9uAx.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-CBalSvz7.js → metamask-sdk-SH1hL_jU.js} +1 -1
- package/packages/ui/dist/assets/{mobile-BEteuhF7.js → mobile-Cq71LsIe.js} +1 -1
- package/packages/ui/dist/assets/{more-DBWmXQli.js → more-CzCaTRQT.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-Dg1uUHiL.js → network-placeholder-B7ilsHLj.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-i3AHSiD9.js → nftPlaceholder-DYo4ThqR.js} +1 -1
- package/packages/ui/dist/assets/{off-BtMm0fi2.js → off-CVXtiamx.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-Cb5FlWWg.js → parseSignature-DDqjAA1x.js} +1 -1
- package/packages/ui/dist/assets/{play-store-iKKkXa6a.js → play-store-Bxo5wDfY.js} +1 -1
- package/packages/ui/dist/assets/{plus-CA5NaRtb.js → plus-ByOmN8u0.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-D2kiqR7h.js → qr-code-BLcaF-bG.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-Dcme7R03.js → recycle-horizontal-R32E5-sJ.js} +1 -1
- package/packages/ui/dist/assets/{refresh-Dega3sDp.js → refresh-DV3LEAlt.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-xNkksyWJ.js → reown-logo-CzXV-ULV.js} +1 -1
- package/packages/ui/dist/assets/{search-HYl7NO8x.js → search-C4ArXMR1.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-Cxd6_SiH.js → secp256k1-CigFWhM4.js} +1 -1
- package/packages/ui/dist/assets/{send-CJU8CUAo.js → send-nlnStDog.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-IMUKiUre.js → swapHorizontal-I-zOG8Yz.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-CNYnNJ9-.js → swapHorizontalBold-5VnEg1-A.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-B9VxEYsT.js → swapHorizontalMedium-DK837_PS.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-Dz33l_Jh.js → swapHorizontalRoundedBold-EcrRbYtU.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-CHUmjVJ0.js → swapVertical--ZRk6QJu.js} +1 -1
- package/packages/ui/dist/assets/{telegram-kl9S2mbU.js → telegram-DQOf_6vw.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-U5lhA1Am.js → three-dots-RaCioLRu.js} +1 -1
- package/packages/ui/dist/assets/{twitch-KTEUWXEp.js → twitch-CXMYNleq.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-BHiq8mRg.js → twitterIcon-CafuOSTc.js} +1 -1
- package/packages/ui/dist/assets/{verify-CfN-BXNd.js → verify-B5SZMJAG.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-DwZccetj.js → verify-filled-BrlLx5ry.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-CS-PFqPE.js → w3m-modal-57iUR7pY.js} +1 -1
- package/packages/ui/dist/assets/{wallet-DVlGkhOY.js → wallet-Fu8Tn5wK.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-CvR_iEWX.js → wallet-placeholder-BYpbF3M_.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-8pZBDvVI.js → walletconnect-BT9pozGB.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-ylLEE0Yp.js → warning-circle-BQ8awbZW.js} +1 -1
- package/packages/ui/dist/assets/{x-C_TBsTMj.js → x-D21DQ5BR.js} +1 -1
- package/packages/ui/dist/index.html +1 -1
- package/scripts/check-init.mjs +2 -3
- package/scripts/postinstall.js +14 -324
- package/templates/{dca-rebalancer → default}/.sail/config.json +2 -2
- package/templates/default/AGENTS.md +92 -0
- package/templates/default/README.md +16 -0
- package/templates/default/examples/dca/README.md +16 -0
- package/templates/default/examples/dca/agent.ts +174 -0
- package/templates/{dca-rebalancer/src → default/examples/dca}/mandate.ts +8 -30
- package/templates/{dca-rebalancer → default}/package.json +2 -2
- package/templates/default/src/agent.ts +37 -0
- package/templates/{dca-rebalancer → default}/src/config.ts +8 -1
- package/templates/default/src/mandate.ts +22 -0
- package/packages/ui/dist/assets/cursor-DV7rOqbJ.js +0 -3
- package/templates/dca-rebalancer/AGENTS.md +0 -246
- package/templates/dca-rebalancer/AGENT_PLAYBOOK.md +0 -110
- package/templates/dca-rebalancer/README.md +0 -16
- package/templates/dca-rebalancer/src/agent.ts +0 -253
- /package/templates/{dca-rebalancer → default}/.cursor/rules +0 -0
- /package/templates/{dca-rebalancer → default}/.env.example +0 -0
- /package/templates/{dca-rebalancer → default}/.github/workflows/agent-tick.yml +0 -0
- /package/templates/{dca-rebalancer → default}/.sail/.gitkeep +0 -0
- /package/templates/{dca-rebalancer → default}/.sail/README.md +0 -0
- /package/templates/{dca-rebalancer → default}/CLAUDE.md +0 -0
- /package/templates/{dca-rebalancer → default}/_gitignore +0 -0
- /package/templates/{dca-rebalancer → default}/docs/PERMISSION_MODEL.md +0 -0
- /package/templates/{dca-rebalancer → default}/tsconfig.json +0 -0
- /package/templates/{dca-rebalancer → default}/ui/README.md +0 -0
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Sail agent playbook
|
|
2
|
-
|
|
3
|
-
Operational guide for an agent (or operator) running a Sail Protocol SMA via the
|
|
4
|
-
Sailor SDK/CLI. Read [docs/PERMISSION_MODEL.md](docs/PERMISSION_MODEL.md) first — the
|
|
5
|
-
conjunctive vs selective distinction underpins everything below.
|
|
6
|
-
|
|
7
|
-
Golden rule: **always ask the user before any action that costs gas or moves funds.**
|
|
8
|
-
|
|
9
|
-
## Step 0 — detect capabilities (always first)
|
|
10
|
-
|
|
11
|
-
Before signing or dispatching anything, learn what kernel you're on:
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const caps = await client.capabilities(); // reads DISPATCH_TYPEHASH on-chain
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
or, from the CLI, run the full preflight:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
sailor doctor # kernel model + permission health, read-only, no gas
|
|
21
|
-
sailor doctor --json # machine-readable
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
`sailor doctor` detects the dispatch model, lists registered permissions, and — on a
|
|
25
|
-
conjunctive kernel — flags any permission that does **not** pass through unrelated
|
|
26
|
-
calls (which would brick every dispatch). Fix those before doing anything else.
|
|
27
|
-
|
|
28
|
-
## Decision tree
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
Want to act on the SMA?
|
|
32
|
-
│
|
|
33
|
-
├─ Is the Sail module enabled + account registered? → no: finish onboarding (sailor onboard)
|
|
34
|
-
│
|
|
35
|
-
├─ sailor doctor green? → no: revoke/replace bricking permissions first
|
|
36
|
-
│
|
|
37
|
-
├─ Need a NEW kind of action the permissions don't allow? → yes: register a permission (owner signs)
|
|
38
|
-
│ • Conjunctive: the permission MUST pass through other calls. Prefer ONE
|
|
39
|
-
│ permission per domain that allows everything you need (e.g. a single
|
|
40
|
-
│ approve permission whitelisting all tokens you'll approve).
|
|
41
|
-
│ • Use a clone template when one exists (no Solidity):
|
|
42
|
-
│ getSailDeployment(chainId).cloneTemplates → deployAndAttach
|
|
43
|
-
│
|
|
44
|
-
├─ One-off swap? → client.strategy.swap({from,to,amount,slippage})
|
|
45
|
-
│
|
|
46
|
-
└─ Recurring/automated (DCA, rebalance)? → loop strategy.swap on a schedule;
|
|
47
|
-
approve a larger batch once so most
|
|
48
|
-
iterations are a single swap dispatch.
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Approvals
|
|
52
|
-
|
|
53
|
-
- An ERC-20 `approve` is itself a dispatch and must pass the registered permissions.
|
|
54
|
-
On a conjunctive kernel that means an **approve permission that allows the token +
|
|
55
|
-
spender + amount**, AND every other permission passing the approve through.
|
|
56
|
-
- The bounded-approve template uses **per-token caps** (token value/decimals differ:
|
|
57
|
-
1 DAI = 1e18 vs 1 USDC = 1e6). One global cap can't bound both.
|
|
58
|
-
- `client.strategy.swap` only approves when the current router allowance is below the
|
|
59
|
-
trade size, so steady-state swaps are a single dispatch. Pass `approveAmount` larger
|
|
60
|
-
than `amount` to batch a bigger approval for DCA.
|
|
61
|
-
|
|
62
|
-
### Swap mandates (LiFi)
|
|
63
|
-
|
|
64
|
-
- The swap permission restricts: target = LiFi diamond, selector allowlisted, embedded
|
|
65
|
-
receiver == the account, `minAmount` ≤ cap. Verify the route's selector is
|
|
66
|
-
allowlisted (Base routes commonly use `0x5fd9ae2e`); add others via the permission
|
|
67
|
-
signer if needed.
|
|
68
|
-
- Default slippage is 3% — LiFi's own 0.5% reverts (`CumulativeSlippageTooHigh`) on
|
|
69
|
-
small trades.
|
|
70
|
-
|
|
71
|
-
### Automated jobs
|
|
72
|
-
|
|
73
|
-
- Sequential dispatches: `client.dispatch.single` auto-tracks the manager nonce per
|
|
74
|
-
`(kernel, account)` and waits for the prior bump to propagate before signing the
|
|
75
|
-
next — no manual nonce handling, even on a load-balanced RPC.
|
|
76
|
-
- Use a dedicated RPC endpoint (not a public replica) to minimize read-after-write lag.
|
|
77
|
-
- Pin post-tx reads to the receipt's block; a lagging node can otherwise make a
|
|
78
|
-
confirmed action look failed.
|
|
79
|
-
|
|
80
|
-
## Failure-mode catalog
|
|
81
|
-
|
|
82
|
-
Every dispatch failure is decoded by the SDK — `client.dispatch.single` rethrows
|
|
83
|
-
reverts already explained, and you can decode any raw revert with
|
|
84
|
-
`explainKernelRevert(err)` / `decodeKernelError(data)`. Common ones:
|
|
85
|
-
|
|
86
|
-
| Error (selector) | What it means | Fix |
|
|
87
|
-
|---|---|---|
|
|
88
|
-
| `InvalidManagerSignature` (`0xeb6942f1`) | The signed EIP-712 Dispatch didn't recover to the registered manager. | Almost always a **stale manager nonce** (RPC lag or two dispatches signed against the same nonce) — re-read `managerNonces` and re-sign; `dispatch.single` now handles this. Or the **wrong Dispatch struct** for this kernel — use `capabilities()`. |
|
|
89
|
-
| `PermissionDenied(permission)` | A registered permission's `evaluate()` returned false / reverted / ran out of gas. | On a **conjunctive** kernel, a permission that doesn't pass through unrelated calls bricks everything — run `sailor doctor` and revoke/replace it. Otherwise the call genuinely violates that permission's bounds. |
|
|
90
|
-
| `NoPermissionsRegistered(account)` | Account has zero permissions; kernel denies by default. | Register at least one permission (owner signs). |
|
|
91
|
-
| `PermissionNotRegistered(permission)` | Named permission isn't registered. | Register it; or on a conjunctive kernel drop the permission arg (the SDK does). |
|
|
92
|
-
| `SessionInactive(account)` | Manager session is revoked. | `session.activate` before dispatching. |
|
|
93
|
-
| `DeadlineExpired(deadline,current)` | Signature deadline is in the past. | Sign with a deadline comfortably ahead of `block.timestamp`. |
|
|
94
|
-
| `SafeExecutionFailed()` | Permission passed, but the target call itself reverted. | Usually slippage too tight, insufficient allowance/balance, or a failing route — not a permission problem. |
|
|
95
|
-
| `ModuleNotEnabled()` | Sail module not enabled on the Safe. | Complete onboarding (enable the module) first. |
|
|
96
|
-
| `ProtocolPaused()` | Governance paused the protocol. | Wait for unpause. |
|
|
97
|
-
| `NotManager(caller,expected)` | Submitter isn't the registered manager. | Submit from the manager key. |
|
|
98
|
-
| `TooManyPermissions(account,limit)` | Per-account permission cap reached. | Revoke an unused permission first. |
|
|
99
|
-
|
|
100
|
-
When in doubt, the SDK hint string (in `error.kernelError.hint`) names the likely
|
|
101
|
-
cause and fix.
|
|
102
|
-
|
|
103
|
-
## Quick reference (SDK)
|
|
104
|
-
|
|
105
|
-
- `client.capabilities()` — detect dispatch model.
|
|
106
|
-
- `client.dispatch.single(safe, permission, call, manager, opts?)` — nonce-safe single dispatch (`opts`: `nonce`, `awaitNonce`, `gas`, `deadline`).
|
|
107
|
-
- `client.strategy.swap(safe, {from,to,amount,slippage,swapPermission?,approveAmount?}, manager)` — approve-when-low + LiFi swap.
|
|
108
|
-
- `explainKernelRevert(err)` / `decodeKernelError(data)` — human-readable revert.
|
|
109
|
-
- `getSailDeployment(chainId).cloneTemplates` — wizard-ready clone templates + their `initialize()` params.
|
|
110
|
-
- CLI: `sailor capabilities` (feasibility map), `sailor doctor` (preflight: model, permissions, RPC + gas), `sailor onboard`, `sailor mandate …`, `sailor station start`.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# DCA Rebalancer — Sail Protocol Agent
|
|
2
|
-
|
|
3
|
-
This folder is your Sail agent. It DCA-rebalances a token basket on a schedule.
|
|
4
|
-
|
|
5
|
-
Open this folder in **Claude Code**, **Cursor**, or **Codex** (or any LLM-powered IDE) and say:
|
|
6
|
-
|
|
7
|
-
> start
|
|
8
|
-
|
|
9
|
-
Your AI coding assistant will walk you through every step — from network and wallet setup to your
|
|
10
|
-
first on-chain tick. See `AGENTS.md` for the details; no manual config needed.
|
|
11
|
-
|
|
12
|
-
## Project layout
|
|
13
|
-
|
|
14
|
-
- `.sail/config.json` is the local project manifest.
|
|
15
|
-
- `.sail/keys/` stores the encrypted agent wallet and mandate signer keys when local signing is used.
|
|
16
|
-
- `.sail/state/` is for persistent agent state, audit logs, and tx history.
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DCA-rebalancer agent — Base mainnet, selective SailKernel.
|
|
3
|
-
*
|
|
4
|
-
* Strategy: on each tick, if the SMA holds enough USDC, swap a fixed amount
|
|
5
|
-
* into WETH via Uniswap V3 SwapRouter02.
|
|
6
|
-
*
|
|
7
|
-
* Slippage protection: QuoterV2 is called off-chain before each swap.
|
|
8
|
-
* amountOutMinimum = expectedOut × (1 − SLIPPAGE_BPS / 10 000)
|
|
9
|
-
* If QuoterV2 is unavailable or returns 0, the agent skips the tick entirely
|
|
10
|
-
* (fail closed — never submits with amountOutMinimum = 0).
|
|
11
|
-
*
|
|
12
|
-
* Selective kernel: the runner supports dispatchBatch, but this agent returns at
|
|
13
|
-
* most ONE dispatch per tick for simplicity:
|
|
14
|
-
* - An approve, if the router allowance is insufficient.
|
|
15
|
-
* - A swap, if allowance is sufficient and a valid quote is available.
|
|
16
|
-
* Next tick handles the other step.
|
|
17
|
-
*
|
|
18
|
-
* The agent does NOT call dispatch.single() itself — it returns intent objects
|
|
19
|
-
* ({calls, txHash:'0x', ...}) and the runner submits them.
|
|
20
|
-
*
|
|
21
|
-
* PERMISSION ROUTING (default — probe path):
|
|
22
|
-
* This template returns plain Dispatch objects with no `permission` field. The
|
|
23
|
-
* runner automatically probes each registered permission via off-chain evaluate()
|
|
24
|
-
* and routes each call to the first permission that accepts it. This is the
|
|
25
|
-
* recommended default: zero agent-side knowledge of permission addresses required.
|
|
26
|
-
*
|
|
27
|
-
* OPTIONAL OVERRIDE (skip probe for a known permission):
|
|
28
|
-
* If the agent knows exactly which permission governs a call, it can set the
|
|
29
|
-
* optional `permission` field on the returned Dispatch to skip the probe:
|
|
30
|
-
*
|
|
31
|
-
* import type { Address } from "viem";
|
|
32
|
-
* const MY_SWAP_PERMISSION = "0x..." as Address;
|
|
33
|
-
* return [{ ...dispatch, permission: MY_SWAP_PERMISSION }];
|
|
34
|
-
*
|
|
35
|
-
* This is an optimisation only — the probe path is equally correct.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import type { Agent, AgentContext, Call, Dispatch } from "@sail/sdk";
|
|
39
|
-
import { encodeFunctionData, type PublicClient } from "viem";
|
|
40
|
-
import {
|
|
41
|
-
ALLOWED_TOKENS,
|
|
42
|
-
MIN_USDC_TO_SWAP,
|
|
43
|
-
QUOTER_V2,
|
|
44
|
-
REBALANCE_THRESHOLD,
|
|
45
|
-
SLIPPAGE_BPS,
|
|
46
|
-
SWAP_AMOUNT_USDC,
|
|
47
|
-
SWAP_FEE_TIER,
|
|
48
|
-
SWAP_ROUTER,
|
|
49
|
-
} from "./mandate.js";
|
|
50
|
-
|
|
51
|
-
// ── ABI fragments ─────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
const ERC20_ABI = [
|
|
54
|
-
{
|
|
55
|
-
name: "allowance",
|
|
56
|
-
type: "function",
|
|
57
|
-
stateMutability: "view",
|
|
58
|
-
inputs: [
|
|
59
|
-
{ name: "owner", type: "address" },
|
|
60
|
-
{ name: "spender", type: "address" },
|
|
61
|
-
],
|
|
62
|
-
outputs: [{ type: "uint256" }],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "approve",
|
|
66
|
-
type: "function",
|
|
67
|
-
stateMutability: "nonpayable",
|
|
68
|
-
inputs: [
|
|
69
|
-
{ name: "spender", type: "address" },
|
|
70
|
-
{ name: "amount", type: "uint256" },
|
|
71
|
-
],
|
|
72
|
-
outputs: [{ type: "bool" }],
|
|
73
|
-
},
|
|
74
|
-
] as const;
|
|
75
|
-
|
|
76
|
-
// Uniswap V3 QuoterV2 — quoteExactInputSingle (struct-params variant).
|
|
77
|
-
// Called via eth_call (publicClient.simulateContract) since the function
|
|
78
|
-
// is marked nonpayable but is safe to simulate as a read.
|
|
79
|
-
const QUOTER_V2_ABI = [
|
|
80
|
-
{
|
|
81
|
-
name: "quoteExactInputSingle",
|
|
82
|
-
type: "function",
|
|
83
|
-
stateMutability: "nonpayable",
|
|
84
|
-
inputs: [
|
|
85
|
-
{
|
|
86
|
-
name: "params",
|
|
87
|
-
type: "tuple",
|
|
88
|
-
components: [
|
|
89
|
-
{ name: "tokenIn", type: "address" },
|
|
90
|
-
{ name: "tokenOut", type: "address" },
|
|
91
|
-
{ name: "amountIn", type: "uint256" },
|
|
92
|
-
{ name: "fee", type: "uint24" },
|
|
93
|
-
{ name: "sqrtPriceLimitX96", type: "uint160" },
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
outputs: [
|
|
98
|
-
{ name: "amountOut", type: "uint256" },
|
|
99
|
-
{ name: "sqrtPriceX96After", type: "uint160" },
|
|
100
|
-
{ name: "initializedTicksCrossed", type: "uint32" },
|
|
101
|
-
{ name: "gasEstimate", type: "uint256" },
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
] as const;
|
|
105
|
-
|
|
106
|
-
const SWAP_ROUTER_ABI = [
|
|
107
|
-
{
|
|
108
|
-
name: "exactInputSingle",
|
|
109
|
-
type: "function",
|
|
110
|
-
stateMutability: "payable",
|
|
111
|
-
inputs: [
|
|
112
|
-
{
|
|
113
|
-
name: "params",
|
|
114
|
-
type: "tuple",
|
|
115
|
-
components: [
|
|
116
|
-
{ name: "tokenIn", type: "address" },
|
|
117
|
-
{ name: "tokenOut", type: "address" },
|
|
118
|
-
{ name: "fee", type: "uint24" },
|
|
119
|
-
{ name: "recipient", type: "address" },
|
|
120
|
-
{ name: "amountIn", type: "uint256" },
|
|
121
|
-
{ name: "amountOutMinimum", type: "uint256" },
|
|
122
|
-
{ name: "sqrtPriceLimitX96", type: "uint160" },
|
|
123
|
-
],
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
outputs: [{ name: "amountOut", type: "uint256" }],
|
|
127
|
-
},
|
|
128
|
-
] as const;
|
|
129
|
-
|
|
130
|
-
// ── Intent builder ────────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
/** Wrap a single EVM call as a Dispatch intent (no txHash yet — runner submits). */
|
|
133
|
-
function intent(call: Call): Dispatch {
|
|
134
|
-
return { txHash: "0x", calls: [call], success: false, gasUsed: 0n };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Agent ─────────────────────────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
export const agent: Agent = {
|
|
140
|
-
name: "dca-rebalancer",
|
|
141
|
-
description:
|
|
142
|
-
`DCA into WETH with USDC on Base via Uniswap V3. ` +
|
|
143
|
-
`Rebalances when allocation drift exceeds ${REBALANCE_THRESHOLD * 100}%. ` +
|
|
144
|
-
`Slippage tolerance: ${SLIPPAGE_BPS / 100}%.`,
|
|
145
|
-
|
|
146
|
-
async tick(ctx: AgentContext): Promise<Dispatch[]> {
|
|
147
|
-
const { safe } = ctx;
|
|
148
|
-
|
|
149
|
-
ctx.log(`tick — block ${ctx.blockNumber}, sma ${safe}`);
|
|
150
|
-
|
|
151
|
-
// publicClient is injected by the runner via ctx.data._publicClient
|
|
152
|
-
// so the agent can make arbitrary on-chain reads (allowance, QuoterV2).
|
|
153
|
-
const pc = ctx.data._publicClient as PublicClient | undefined;
|
|
154
|
-
if (!pc) {
|
|
155
|
-
ctx.log("no publicClient in ctx.data — skipping tick");
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const usdc = ALLOWED_TOKENS[0]!;
|
|
160
|
-
const weth = ALLOWED_TOKENS[1]!;
|
|
161
|
-
|
|
162
|
-
// ── Step 1: Check USDC balance ────────────────────────────────────────────
|
|
163
|
-
const usdcBalance = await ctx.read.balance(usdc);
|
|
164
|
-
ctx.log(`USDC balance: ${usdcBalance} (min to swap: ${MIN_USDC_TO_SWAP})`);
|
|
165
|
-
|
|
166
|
-
if (usdcBalance < MIN_USDC_TO_SWAP) {
|
|
167
|
-
ctx.log("USDC balance below minimum — skipping tick");
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ── Step 2: Check allowance — approve first if needed ─────────────────────
|
|
172
|
-
const allowance = await pc.readContract({
|
|
173
|
-
address: usdc,
|
|
174
|
-
abi: ERC20_ABI,
|
|
175
|
-
functionName: "allowance",
|
|
176
|
-
args: [safe, SWAP_ROUTER],
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (allowance < SWAP_AMOUNT_USDC) {
|
|
180
|
-
ctx.log(`allowance (${allowance}) < swap amount (${SWAP_AMOUNT_USDC}) — submitting approve`);
|
|
181
|
-
// Approve max uint256 once so subsequent ticks skip this step.
|
|
182
|
-
const approveCall: Call = {
|
|
183
|
-
target: usdc,
|
|
184
|
-
value: 0n,
|
|
185
|
-
data: encodeFunctionData({
|
|
186
|
-
abi: ERC20_ABI,
|
|
187
|
-
functionName: "approve",
|
|
188
|
-
args: [SWAP_ROUTER, 2n ** 256n - 1n],
|
|
189
|
-
}),
|
|
190
|
-
};
|
|
191
|
-
return [intent(approveCall)];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Step 3: Quote via QuoterV2 — fail closed ──────────────────────────────
|
|
195
|
-
let expectedOut: bigint;
|
|
196
|
-
try {
|
|
197
|
-
const result = await pc.simulateContract({
|
|
198
|
-
address: QUOTER_V2,
|
|
199
|
-
abi: QUOTER_V2_ABI,
|
|
200
|
-
functionName: "quoteExactInputSingle",
|
|
201
|
-
args: [
|
|
202
|
-
{
|
|
203
|
-
tokenIn: usdc,
|
|
204
|
-
tokenOut: weth,
|
|
205
|
-
amountIn: SWAP_AMOUNT_USDC,
|
|
206
|
-
fee: SWAP_FEE_TIER,
|
|
207
|
-
sqrtPriceLimitX96: 0n,
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
});
|
|
211
|
-
expectedOut = (result.result as [bigint, bigint, number, bigint])[0];
|
|
212
|
-
} catch (e) {
|
|
213
|
-
ctx.log(`QuoterV2 unavailable: ${(e as Error).message.slice(0, 100)} — skipping tick (fail closed)`);
|
|
214
|
-
return [];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (expectedOut === 0n) {
|
|
218
|
-
ctx.log("QuoterV2 returned 0 expected output — skipping tick (fail closed)");
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ── Step 4: Compute amountOutMinimum with slippage ────────────────────────
|
|
223
|
-
const minOut = (expectedOut * BigInt(10_000 - SLIPPAGE_BPS)) / 10_000n;
|
|
224
|
-
ctx.log(
|
|
225
|
-
`quote: ${expectedOut} wei WETH, minOut (${SLIPPAGE_BPS / 100}% slippage): ${minOut}`,
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
// ── Step 5: Encode swap call ──────────────────────────────────────────────
|
|
229
|
-
const swapCall: Call = {
|
|
230
|
-
// Uniswap V3 SwapRouter02 on Base
|
|
231
|
-
target: SWAP_ROUTER,
|
|
232
|
-
value: 0n,
|
|
233
|
-
data: encodeFunctionData({
|
|
234
|
-
abi: SWAP_ROUTER_ABI,
|
|
235
|
-
functionName: "exactInputSingle",
|
|
236
|
-
args: [
|
|
237
|
-
{
|
|
238
|
-
tokenIn: usdc,
|
|
239
|
-
tokenOut: weth,
|
|
240
|
-
fee: SWAP_FEE_TIER,
|
|
241
|
-
recipient: safe, // output stays in the SMA
|
|
242
|
-
amountIn: SWAP_AMOUNT_USDC,
|
|
243
|
-
amountOutMinimum: minOut, // slippage-protected — never 0
|
|
244
|
-
sqrtPriceLimitX96: 0n,
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
}),
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
ctx.log(`submitting swap: ${SWAP_AMOUNT_USDC} USDC → WETH, minOut=${minOut}`);
|
|
251
|
-
return [intent(swapCall)];
|
|
252
|
-
},
|
|
253
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|