@dev.sail.money/sailor 0.0.2-12 → 0.0.2-15

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.
Files changed (124) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +27 -26
  3. package/package.json +1 -1
  4. package/packages/cli/dist/index.cjs +20 -40
  5. package/packages/sdk/dist/intelligence.d.ts +1 -1
  6. package/packages/sdk/dist/intelligence.js +1 -1
  7. package/packages/ui/dist/assets/{add-DaJhwIBV.js → add-BHtIsGoW.js} +1 -1
  8. package/packages/ui/dist/assets/{all-wallets-BUxsqWXi.js → all-wallets-BH8IiQp_.js} +1 -1
  9. package/packages/ui/dist/assets/{app-store-DkltwTqE.js → app-store-C7je0Hvt.js} +1 -1
  10. package/packages/ui/dist/assets/{apple-owVOeaIT.js → apple-C24YGi7n.js} +1 -1
  11. package/packages/ui/dist/assets/{arrow-bottom-D2mmNJve.js → arrow-bottom-NmB75e6T.js} +1 -1
  12. package/packages/ui/dist/assets/{arrow-bottom-circle-CbNYijx-.js → arrow-bottom-circle-D9vkbcGm.js} +1 -1
  13. package/packages/ui/dist/assets/{arrow-left-DJB61s4C.js → arrow-left-BXX7egmu.js} +1 -1
  14. package/packages/ui/dist/assets/{arrow-right-BBrsQ9R4.js → arrow-right-CL5l1ER5.js} +1 -1
  15. package/packages/ui/dist/assets/{arrow-top-Cil6bOc8.js → arrow-top-B3NcsHvl.js} +1 -1
  16. package/packages/ui/dist/assets/{bank-CbwEmRo3.js → bank-DyxCAL_C.js} +1 -1
  17. package/packages/ui/dist/assets/{basic-CLNfjw3m.js → basic-CnjXr1WW.js} +1 -1
  18. package/packages/ui/dist/assets/{browser-B5TtF4Pb.js → browser-C1sQ7PjQ.js} +1 -1
  19. package/packages/ui/dist/assets/{card-CO7BVB-C.js → card-DUcWbZgm.js} +1 -1
  20. package/packages/ui/dist/assets/{ccip-2W7K3_J3.js → ccip-ZC8164Ut.js} +1 -1
  21. package/packages/ui/dist/assets/{checkmark-BEtSHq9m.js → checkmark-BFIb0gZI.js} +1 -1
  22. package/packages/ui/dist/assets/{checkmark-bold-D9xGHzPE.js → checkmark-bold-GxOovLvw.js} +1 -1
  23. package/packages/ui/dist/assets/{chevron-bottom-BDztht6i.js → chevron-bottom-BWFm5iOO.js} +1 -1
  24. package/packages/ui/dist/assets/{chevron-left-EV4GFNbc.js → chevron-left-Dw1vrfPS.js} +1 -1
  25. package/packages/ui/dist/assets/{chevron-right-B4_bB9oR.js → chevron-right-Lj-4a_f7.js} +1 -1
  26. package/packages/ui/dist/assets/{chevron-top-D54xPNzF.js → chevron-top-CNtyUs8e.js} +1 -1
  27. package/packages/ui/dist/assets/{chrome-store-DYUpAJJq.js → chrome-store-BFjfxT2g.js} +1 -1
  28. package/packages/ui/dist/assets/{clock-Ca1T1Soz.js → clock-upNWT1cC.js} +1 -1
  29. package/packages/ui/dist/assets/{close-BZqWjurK.js → close-BXOvqFtp.js} +1 -1
  30. package/packages/ui/dist/assets/{coinPlaceholder-e6fl2XDo.js → coinPlaceholder-IDofua41.js} +1 -1
  31. package/packages/ui/dist/assets/{compass-DCLC7zIh.js → compass-Du2xGTuZ.js} +1 -1
  32. package/packages/ui/dist/assets/{copy-Th2AaD-O.js → copy-thfTJs3L.js} +1 -1
  33. package/packages/ui/dist/assets/{core-Ckx_cyuH.js → core-C8jSlwHY.js} +3 -3
  34. package/packages/ui/dist/assets/cursor-Dc-Hxk5X.js +3 -0
  35. package/packages/ui/dist/assets/{cursor-transparent-BKHeABKB.js → cursor-transparent-DGzNzXxd.js} +1 -1
  36. package/packages/ui/dist/assets/{desktop-CBjY8t6F.js → desktop-BozY9d6T.js} +1 -1
  37. package/packages/ui/dist/assets/{disconnect-DbSs2cli.js → disconnect-B-oC8Fzu.js} +1 -1
  38. package/packages/ui/dist/assets/{discord-ZlLOAUkM.js → discord-CpgvcuGY.js} +1 -1
  39. package/packages/ui/dist/assets/{etherscan-CKUrqWYN.js → etherscan-DB-rL796.js} +1 -1
  40. package/packages/ui/dist/assets/{events-CiKP71cK.js → events-i9ztcj9W.js} +1 -1
  41. package/packages/ui/dist/assets/{exclamation-triangle-DA1QzFiO.js → exclamation-triangle-Ct0mzv3Y.js} +1 -1
  42. package/packages/ui/dist/assets/{extension-BVJkmvpJ.js → extension-CubWAVae.js} +1 -1
  43. package/packages/ui/dist/assets/{external-link-D_bsR7B2.js → external-link-CgYSHh7f.js} +1 -1
  44. package/packages/ui/dist/assets/{facebook-CmFmhojx.js → facebook-DI_kGikU.js} +1 -1
  45. package/packages/ui/dist/assets/{fallback-Ofl6uSnB.js → fallback-CtKplO-p.js} +1 -1
  46. package/packages/ui/dist/assets/{farcaster-Co-M3Ss8.js → farcaster-PEZAXYqi.js} +1 -1
  47. package/packages/ui/dist/assets/{filters-B1WwNaFU.js → filters-D6NPvINV.js} +1 -1
  48. package/packages/ui/dist/assets/{github-CP4fP6gn.js → github-BRk_ww3A.js} +1 -1
  49. package/packages/ui/dist/assets/{google-CsOIXJ6V.js → google-3tO0AiTd.js} +1 -1
  50. package/packages/ui/dist/assets/{help-circle-DiMkomdF.js → help-circle-DtbseC_9.js} +1 -1
  51. package/packages/ui/dist/assets/{id-lmscL5LX.js → id-DNQF-HQS.js} +1 -1
  52. package/packages/ui/dist/assets/{image-B-ubJrY5.js → image-D4l2W9gw.js} +1 -1
  53. package/packages/ui/dist/assets/{index-DVgfCzCo.js → index-A8Mpr5Rm.js} +1 -1
  54. package/packages/ui/dist/assets/{index-BaukYv-x.js → index-BnYYo2S0.js} +1 -1
  55. package/packages/ui/dist/assets/{index-CZR1Qjhs.js → index-D-PqJKnq.js} +1 -1
  56. package/packages/ui/dist/assets/{index-Q2Yai4Fe.js → index-L6zhWbjO.js} +70 -70
  57. package/packages/ui/dist/assets/{index-Dbh5V1Z0.js → index-cl7Zazmz.js} +1 -1
  58. package/packages/ui/dist/assets/{index-CF0KMmke.js → index-vIXQ5sb2.js} +3 -3
  59. package/packages/ui/dist/assets/{index.es-C78cE5SI.js → index.es-BxPfiRyX.js} +4 -4
  60. package/packages/ui/dist/assets/{info-Cqg57EVo.js → info-DAwJNl3r.js} +1 -1
  61. package/packages/ui/dist/assets/{info-circle-DkeSWNKV.js → info-circle-lvyXhzfD.js} +1 -1
  62. package/packages/ui/dist/assets/{lightbulb-DNlO4qKh.js → lightbulb-BkrBfCQo.js} +1 -1
  63. package/packages/ui/dist/assets/{mail-kVQ8Jb9Y.js → mail-CrRh9uAx.js} +1 -1
  64. package/packages/ui/dist/assets/{metamask-sdk-CBalSvz7.js → metamask-sdk-SH1hL_jU.js} +1 -1
  65. package/packages/ui/dist/assets/{mobile-BEteuhF7.js → mobile-Cq71LsIe.js} +1 -1
  66. package/packages/ui/dist/assets/{more-DBWmXQli.js → more-CzCaTRQT.js} +1 -1
  67. package/packages/ui/dist/assets/{network-placeholder-Dg1uUHiL.js → network-placeholder-B7ilsHLj.js} +1 -1
  68. package/packages/ui/dist/assets/{nftPlaceholder-i3AHSiD9.js → nftPlaceholder-DYo4ThqR.js} +1 -1
  69. package/packages/ui/dist/assets/{off-BtMm0fi2.js → off-CVXtiamx.js} +1 -1
  70. package/packages/ui/dist/assets/{parseSignature-Cb5FlWWg.js → parseSignature-DDqjAA1x.js} +1 -1
  71. package/packages/ui/dist/assets/{play-store-iKKkXa6a.js → play-store-Bxo5wDfY.js} +1 -1
  72. package/packages/ui/dist/assets/{plus-CA5NaRtb.js → plus-ByOmN8u0.js} +1 -1
  73. package/packages/ui/dist/assets/{qr-code-D2kiqR7h.js → qr-code-BLcaF-bG.js} +1 -1
  74. package/packages/ui/dist/assets/{recycle-horizontal-Dcme7R03.js → recycle-horizontal-R32E5-sJ.js} +1 -1
  75. package/packages/ui/dist/assets/{refresh-Dega3sDp.js → refresh-DV3LEAlt.js} +1 -1
  76. package/packages/ui/dist/assets/{reown-logo-xNkksyWJ.js → reown-logo-CzXV-ULV.js} +1 -1
  77. package/packages/ui/dist/assets/{search-HYl7NO8x.js → search-C4ArXMR1.js} +1 -1
  78. package/packages/ui/dist/assets/{secp256k1-Cxd6_SiH.js → secp256k1-CigFWhM4.js} +1 -1
  79. package/packages/ui/dist/assets/{send-CJU8CUAo.js → send-nlnStDog.js} +1 -1
  80. package/packages/ui/dist/assets/{swapHorizontal-IMUKiUre.js → swapHorizontal-I-zOG8Yz.js} +1 -1
  81. package/packages/ui/dist/assets/{swapHorizontalBold-CNYnNJ9-.js → swapHorizontalBold-5VnEg1-A.js} +1 -1
  82. package/packages/ui/dist/assets/{swapHorizontalMedium-B9VxEYsT.js → swapHorizontalMedium-DK837_PS.js} +1 -1
  83. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-Dz33l_Jh.js → swapHorizontalRoundedBold-EcrRbYtU.js} +1 -1
  84. package/packages/ui/dist/assets/{swapVertical-CHUmjVJ0.js → swapVertical--ZRk6QJu.js} +1 -1
  85. package/packages/ui/dist/assets/{telegram-kl9S2mbU.js → telegram-DQOf_6vw.js} +1 -1
  86. package/packages/ui/dist/assets/{three-dots-U5lhA1Am.js → three-dots-RaCioLRu.js} +1 -1
  87. package/packages/ui/dist/assets/{twitch-KTEUWXEp.js → twitch-CXMYNleq.js} +1 -1
  88. package/packages/ui/dist/assets/{twitterIcon-BHiq8mRg.js → twitterIcon-CafuOSTc.js} +1 -1
  89. package/packages/ui/dist/assets/{verify-CfN-BXNd.js → verify-B5SZMJAG.js} +1 -1
  90. package/packages/ui/dist/assets/{verify-filled-DwZccetj.js → verify-filled-BrlLx5ry.js} +1 -1
  91. package/packages/ui/dist/assets/{w3m-modal-CS-PFqPE.js → w3m-modal-57iUR7pY.js} +1 -1
  92. package/packages/ui/dist/assets/{wallet-DVlGkhOY.js → wallet-Fu8Tn5wK.js} +1 -1
  93. package/packages/ui/dist/assets/{wallet-placeholder-CvR_iEWX.js → wallet-placeholder-BYpbF3M_.js} +1 -1
  94. package/packages/ui/dist/assets/{walletconnect-8pZBDvVI.js → walletconnect-BT9pozGB.js} +1 -1
  95. package/packages/ui/dist/assets/{warning-circle-ylLEE0Yp.js → warning-circle-BQ8awbZW.js} +1 -1
  96. package/packages/ui/dist/assets/{x-C_TBsTMj.js → x-D21DQ5BR.js} +1 -1
  97. package/packages/ui/dist/index.html +1 -1
  98. package/scripts/check-init.mjs +2 -3
  99. package/scripts/postinstall.js +14 -324
  100. package/templates/{dca-rebalancer → default}/.sail/config.json +2 -2
  101. package/templates/default/AGENTS.md +92 -0
  102. package/templates/default/README.md +16 -0
  103. package/templates/default/examples/dca/README.md +16 -0
  104. package/templates/default/examples/dca/agent.ts +174 -0
  105. package/templates/{dca-rebalancer/src → default/examples/dca}/mandate.ts +8 -30
  106. package/templates/{dca-rebalancer → default}/package.json +2 -2
  107. package/templates/default/src/agent.ts +37 -0
  108. package/templates/{dca-rebalancer → default}/src/config.ts +8 -1
  109. package/templates/default/src/mandate.ts +22 -0
  110. package/packages/ui/dist/assets/cursor-DV7rOqbJ.js +0 -3
  111. package/templates/dca-rebalancer/AGENTS.md +0 -246
  112. package/templates/dca-rebalancer/AGENT_PLAYBOOK.md +0 -110
  113. package/templates/dca-rebalancer/README.md +0 -16
  114. package/templates/dca-rebalancer/src/agent.ts +0 -253
  115. /package/templates/{dca-rebalancer → default}/.cursor/rules +0 -0
  116. /package/templates/{dca-rebalancer → default}/.env.example +0 -0
  117. /package/templates/{dca-rebalancer → default}/.github/workflows/agent-tick.yml +0 -0
  118. /package/templates/{dca-rebalancer → default}/.sail/.gitkeep +0 -0
  119. /package/templates/{dca-rebalancer → default}/.sail/README.md +0 -0
  120. /package/templates/{dca-rebalancer → default}/CLAUDE.md +0 -0
  121. /package/templates/{dca-rebalancer → default}/_gitignore +0 -0
  122. /package/templates/{dca-rebalancer → default}/docs/PERMISSION_MODEL.md +0 -0
  123. /package/templates/{dca-rebalancer → default}/tsconfig.json +0 -0
  124. /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