@dev.sail.money/sailor 1.3.0-95 → 1.4.0-242
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 +10 -7
- package/LICENSE +1 -1
- package/README.md +64 -472
- package/package.json +12 -8
- package/packages/cli/README.md +1 -1
- package/packages/cli/dist/index.cjs +1633 -535
- package/packages/cli/dist/server.cjs +19791 -21738
- package/packages/sdk/README.md +1 -1
- package/packages/sdk/dist/abis/ConfigurablePermission.d.ts +130 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.d.ts.map +1 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.js +79 -0
- package/packages/sdk/dist/abis/ConfigurablePermission.js.map +1 -0
- package/packages/sdk/dist/abis/SailGovernance.d.ts +6 -5
- package/packages/sdk/dist/abis/SailGovernance.d.ts.map +1 -1
- package/packages/sdk/dist/abis/SailGovernance.js +6 -5
- package/packages/sdk/dist/abis/SailGovernance.js.map +1 -1
- package/packages/sdk/dist/abis/SailKernel.d.ts +42 -0
- package/packages/sdk/dist/abis/SailKernel.d.ts.map +1 -1
- package/packages/sdk/dist/abis/SailKernel.js +34 -0
- package/packages/sdk/dist/abis/SailKernel.js.map +1 -1
- package/packages/sdk/dist/abis/index.d.ts +1 -0
- package/packages/sdk/dist/abis/index.d.ts.map +1 -1
- package/packages/sdk/dist/abis/index.js +1 -0
- package/packages/sdk/dist/abis/index.js.map +1 -1
- package/packages/sdk/dist/chains.d.ts +0 -7
- package/packages/sdk/dist/chains.d.ts.map +1 -1
- package/packages/sdk/dist/chains.js +81 -32
- package/packages/sdk/dist/chains.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +1 -1
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +137 -16
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/deployments.d.ts +6 -4
- package/packages/sdk/dist/deployments.d.ts.map +1 -1
- package/packages/sdk/dist/deployments.js +120 -118
- package/packages/sdk/dist/deployments.js.map +1 -1
- package/packages/sdk/dist/eip712.d.ts +84 -0
- package/packages/sdk/dist/eip712.d.ts.map +1 -1
- package/packages/sdk/dist/eip712.js +148 -0
- package/packages/sdk/dist/eip712.js.map +1 -1
- package/packages/sdk/dist/fees.d.ts +59 -10
- package/packages/sdk/dist/fees.d.ts.map +1 -1
- package/packages/sdk/dist/fees.js +74 -41
- package/packages/sdk/dist/fees.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +5 -3
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +4 -3
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/intelligence.d.ts +7 -2
- package/packages/sdk/dist/intelligence.d.ts.map +1 -1
- package/packages/sdk/dist/intelligence.js +2 -2
- package/packages/sdk/dist/intelligence.js.map +1 -1
- package/packages/sdk/dist/safe.d.ts +49 -0
- package/packages/sdk/dist/safe.d.ts.map +1 -1
- package/packages/sdk/dist/safe.js +74 -0
- package/packages/sdk/dist/safe.js.map +1 -1
- package/packages/sdk/dist/signing.d.ts +14 -0
- package/packages/sdk/dist/signing.d.ts.map +1 -1
- package/packages/sdk/package.json +6 -2
- package/packages/ui/dist/assets/{add-CLs0-B0p.js → add-CBbu2pe_.js} +2 -2
- package/packages/ui/dist/assets/{all-wallets-CDOk28sj.js → all-wallets-C1oy4NUu.js} +2 -2
- package/packages/ui/dist/assets/{app-store-DVzpi1qN.js → app-store-DKqyf0Zv.js} +1 -1
- package/packages/ui/dist/assets/{apple-C9U9iH1l.js → apple-DyoIbCFy.js} +2 -2
- package/packages/ui/dist/assets/{arrow-bottom-circle-jxial9Si.js → arrow-bottom-circle-CMS-OqZQ.js} +2 -2
- package/packages/ui/dist/assets/arrow-bottom-pUWeEqYu.js +8 -0
- package/packages/ui/dist/assets/arrow-left-B81HMJNL.js +8 -0
- package/packages/ui/dist/assets/arrow-right-Z5czoKlC.js +8 -0
- package/packages/ui/dist/assets/arrow-top-C6ym1oll.js +8 -0
- package/packages/ui/dist/assets/{bank-B98it1qN.js → bank-B41nlslf.js} +2 -2
- package/packages/ui/dist/assets/{basic-DsgeyPNu.js → basic-CWqOb7CV.js} +177 -177
- package/packages/ui/dist/assets/{browser-j4vCeHwE.js → browser-DDKdqoZ9.js} +2 -2
- package/packages/ui/dist/assets/{card-CzxbMvEd.js → card-BgOmTajq.js} +2 -2
- package/packages/ui/dist/assets/ccip-CtJUKWRd.js +1 -0
- package/packages/ui/dist/assets/{checkmark-D5sWBbk3.js → checkmark-CGqQipH4.js} +2 -2
- package/packages/ui/dist/assets/{checkmark-bold-BW6w3KCC.js → checkmark-bold-Ci5xf90j.js} +2 -2
- package/packages/ui/dist/assets/chevron-bottom-D7Xert17.js +8 -0
- package/packages/ui/dist/assets/chevron-left-BGbUebR2.js +8 -0
- package/packages/ui/dist/assets/chevron-right-xVsESnHU.js +8 -0
- package/packages/ui/dist/assets/chevron-top-DEA9cR6v.js +8 -0
- package/packages/ui/dist/assets/{chrome-store-B1OMtzC-.js → chrome-store-BsyI-4PI.js} +2 -2
- package/packages/ui/dist/assets/{clock-e0HueehS.js → clock-Dct9oQ3M.js} +2 -2
- package/packages/ui/dist/assets/{close-Cy1ytr26.js → close-BvSCnI2y.js} +2 -2
- package/packages/ui/dist/assets/{coinPlaceholder-DqJu3ZPV.js → coinPlaceholder-p7RiWkon.js} +2 -2
- package/packages/ui/dist/assets/{compass-XDz_N2Hg.js → compass-BUeeZqX4.js} +2 -2
- package/packages/ui/dist/assets/{copy-DmDo3Nob.js → copy-C5jXbxLq.js} +2 -2
- package/packages/ui/dist/assets/core-CiAm42Mq.js +907 -0
- package/packages/ui/dist/assets/cursor-C0T7JWFO.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-DF2aA10D.js → cursor-transparent-BZp3-JmA.js} +2 -2
- package/packages/ui/dist/assets/{desktop-CVydOWLn.js → desktop-BIui_Lzb.js} +2 -2
- package/packages/ui/dist/assets/{disconnect-BuN2Whnz.js → disconnect-B3Bxdchp.js} +2 -2
- package/packages/ui/dist/assets/{discord-DMNPvTB2.js → discord-VWI0yDyx.js} +2 -2
- package/packages/ui/dist/assets/{etherscan-CacW-93f.js → etherscan-BpBvUT9i.js} +2 -2
- package/packages/ui/dist/assets/events-DQ172AOg.js +1 -0
- package/packages/ui/dist/assets/{exclamation-triangle-B26hIvqO.js → exclamation-triangle-o1TJHIUw.js} +2 -2
- package/packages/ui/dist/assets/{extension-B7rI0gv7.js → extension-4RaLvbh5.js} +2 -2
- package/packages/ui/dist/assets/external-link-BHIwjDgj.js +8 -0
- package/packages/ui/dist/assets/{facebook-BDqbE6kl.js → facebook-Bes5g-wN.js} +2 -2
- package/packages/ui/dist/assets/{farcaster-zQ3oQzZ8.js → farcaster-CiJ-352m.js} +2 -2
- package/packages/ui/dist/assets/{filters-DCglZYsM.js → filters-DMOteumb.js} +2 -2
- package/packages/ui/dist/assets/{github-BH-BYjss.js → github-D2nDec9X.js} +2 -2
- package/packages/ui/dist/assets/{google-DYFWxZbC.js → google-lZZlQZPQ.js} +2 -2
- package/packages/ui/dist/assets/{help-circle-BXH_DtZ_.js → help-circle-D4cSSziR.js} +1 -1
- package/packages/ui/dist/assets/{id-Dr2yTeX1.js → id-BmhXZq5P.js} +2 -2
- package/packages/ui/dist/assets/{image-DeJey1Eg.js → image-Dz4FPeET.js} +2 -2
- package/packages/ui/dist/assets/index-B07bCKJ8.css +1 -0
- package/packages/ui/dist/assets/index-BV8s6ez1.js +1 -0
- package/packages/ui/dist/assets/index-BeUp3nOq.js +1 -0
- package/packages/ui/dist/assets/index-CvRylmZJ.js +16 -0
- package/packages/ui/dist/assets/{index-DVjAiPHI.js → index-DeoQO45V.js} +3 -3
- package/packages/ui/dist/assets/index-Et5S3ZaW.js +47 -0
- package/packages/ui/dist/assets/index-LiKeH8Cd.js +1780 -0
- package/packages/ui/dist/assets/index.es-dfpEui-9.js +26 -0
- package/packages/ui/dist/assets/{info-B91pD5Rw.js → info-77RSvIMg.js} +2 -2
- package/packages/ui/dist/assets/{info-circle-CySl3_on.js → info-circle-CWsII-NN.js} +2 -2
- package/packages/ui/dist/assets/{lightbulb-wwG8LQLo.js → lightbulb-BZXzm7XE.js} +2 -2
- package/packages/ui/dist/assets/{mail-D617YZ8y.js → mail-DMG1YqB0.js} +2 -2
- package/packages/ui/dist/assets/metamask-sdk-K1x_iqcc.js +542 -0
- package/packages/ui/dist/assets/{mobile-CD9KlA0m.js → mobile-DuJfOBSk.js} +2 -2
- package/packages/ui/dist/assets/{more-BQ_rpPXo.js → more-Cs9ySK2F.js} +2 -2
- package/packages/ui/dist/assets/{network-placeholder-BA7nl9e6.js → network-placeholder-CMDPmB1E.js} +2 -2
- package/packages/ui/dist/assets/{nftPlaceholder-D1EaaeDn.js → nftPlaceholder-Cj0f9y2u.js} +2 -2
- package/packages/ui/dist/assets/{off-DnGG35_w.js → off-9zSSsm-e.js} +2 -2
- package/packages/ui/dist/assets/{play-store-C5u2FvYB.js → play-store-CPwIb8Gj.js} +2 -2
- package/packages/ui/dist/assets/{plus-CM4xJkvI.js → plus-BOHQri8o.js} +2 -2
- package/packages/ui/dist/assets/{qr-code-kM3FUIdR.js → qr-code-DqhbK276.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-DWrRWIBM.js → recycle-horizontal-BpGM56Qz.js} +2 -2
- package/packages/ui/dist/assets/{refresh-CztbjK2d.js → refresh-BPzZlhTP.js} +2 -2
- package/packages/ui/dist/assets/{reown-logo-DUHEIodF.js → reown-logo-OCkI0xzo.js} +2 -2
- package/packages/ui/dist/assets/{search-BJtb4mn1.js → search-B1JEJvwq.js} +2 -2
- package/packages/ui/dist/assets/{secp256k1-BjrTZpQx.js → secp256k1-BqUeV9Cb.js} +1 -1
- package/packages/ui/dist/assets/{send-B3_kavG1.js → send-JNsDlNoq.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontal-BrF9POiv.js → swapHorizontal-C3Z1FImK.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontalBold-Dgf8OjXj.js → swapHorizontalBold-Cmdy2iAn.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontalMedium-C6wX0JZj.js → swapHorizontalMedium-DFX57Nn9.js} +2 -2
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-DTAVw6oF.js → swapHorizontalRoundedBold-baB389c8.js} +2 -2
- package/packages/ui/dist/assets/{swapVertical-KGlTFNoi.js → swapVertical-DmAYGfK1.js} +2 -2
- package/packages/ui/dist/assets/{telegram-DnLrK-4y.js → telegram-BHbYE-nI.js} +2 -2
- package/packages/ui/dist/assets/{three-dots-C6_oMJfw.js → three-dots-BwRWl6qJ.js} +2 -2
- package/packages/ui/dist/assets/{twitch-DvPqPD4f.js → twitch-Drx0q6B6.js} +2 -2
- package/packages/ui/dist/assets/{twitterIcon-BZdk82Am.js → twitterIcon-AuJ2KQ6p.js} +2 -2
- package/packages/ui/dist/assets/{verify-CbESD5G0.js → verify-Dkrd3q7o.js} +2 -2
- package/packages/ui/dist/assets/{verify-filled-BRjTrLxw.js → verify-filled-B9e3-nz-.js} +2 -2
- package/packages/ui/dist/assets/{w3m-modal-D9b2TGHc.js → w3m-modal-DtkEjQA7.js} +1 -1
- package/packages/ui/dist/assets/{wallet-B87E5UId.js → wallet-CqYzSiBd.js} +2 -2
- package/packages/ui/dist/assets/{wallet-placeholder-DmssSbYf.js → wallet-placeholder-B05kTN_K.js} +2 -2
- package/packages/ui/dist/assets/{walletconnect-CZkLKWMZ.js → walletconnect-BwoPXLaS.js} +3 -3
- package/packages/ui/dist/assets/{warning-circle-BPTpHB1P.js → warning-circle-DK9Ai8rT.js} +2 -2
- package/packages/ui/dist/assets/{x-D_dEtEDU.js → x-BNdh5DVW.js} +2 -2
- package/packages/ui/dist/index.html +2 -2
- package/templates/default/.agents/skills/sail-mandates/SKILL.md +13 -1
- package/templates/default/.agents/skills/sail-mandates/references/approvals.md +3 -3
- package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +3 -3
- package/templates/default/.agents/skills/sail-onboarding/SKILL.md +2 -2
- package/templates/default/.agents/skills/sail-project-info/SKILL.md +2 -0
- package/templates/default/.agents/skills/sail-servers/SKILL.md +1 -0
- package/templates/default/.agents/skills/sail-swap-quote/SKILL.md +76 -0
- package/templates/default/.agents/skills/sail-template-approve-batch/SKILL.md +98 -0
- package/templates/default/.agents/skills/sail-template-borrow/SKILL.md +68 -0
- package/templates/default/.agents/skills/sail-template-deposit/SKILL.md +61 -0
- package/templates/default/.agents/skills/sail-template-swap/SKILL.md +207 -0
- package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +100 -0
- package/templates/default/.agents/skills/sail-template-transfer/SKILL.md +52 -0
- package/templates/default/.agents/skills/sail-template-withdraw/SKILL.md +50 -0
- package/templates/default/.agents/skills/sail-templates/SKILL.md +142 -0
- package/templates/default/.agents/skills/sail-templates/catalog.mjs +200 -0
- package/templates/default/.agents/skills/sail-templates/deployed.json +133 -0
- package/templates/default/.agents/skills/sail-templates/oracles/IOracle.sol +13 -0
- package/templates/default/.agents/skills/sail-templates/oracles/UniV3TwapOracle.sol +114 -0
- package/templates/default/.agents/skills/sail-templates/oracles/libs/FullMath.sol +62 -0
- package/templates/default/.agents/skills/sail-templates/oracles/libs/TickMath.sol +44 -0
- package/templates/default/.agents/skills/sail-templates/references/config-schemas.md +139 -0
- package/templates/default/.agents/skills/sail-templates/references/reuse-flow.md +139 -0
- package/templates/default/.agents/skills/sail-token-resolve/SKILL.md +174 -0
- package/templates/default/.env.example +9 -0
- package/templates/default/AGENTS.md +21 -2
- package/templates/default/docs/PERMISSION_MODEL.md +2 -2
- package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +4 -0
- package/{examples → templates/default/examples}/custom-mandate/README.md +4 -1
- package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +2 -0
- package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +4 -0
- package/templates/default/scripts/quote-swap.mjs +211 -0
- package/templates/default/scripts/resolve-token.mjs +992 -0
- package/templates/default/scripts/shared-template-addr.mjs +110 -0
- package/templates/default/test/BoundedCallPermission.t.sol +2 -1
- package/docs/PERMISSION_MODEL.md +0 -93
- package/examples/README.md +0 -24
- package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
- package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
- package/examples/lifi-permissions/README.md +0 -53
- package/packages/cli/dist/mcp.cjs +0 -15534
- package/packages/ui/dist/assets/arrow-bottom-DjQ4jGMe.js +0 -8
- package/packages/ui/dist/assets/arrow-left-BjAyI8D1.js +0 -8
- package/packages/ui/dist/assets/arrow-right-BlFO4Tcx.js +0 -8
- package/packages/ui/dist/assets/arrow-top-DJ3oh3DH.js +0 -8
- package/packages/ui/dist/assets/ccip-U9yiu_EK.js +0 -1
- package/packages/ui/dist/assets/chevron-bottom-BbHhmTQq.js +0 -8
- package/packages/ui/dist/assets/chevron-left-D8BDH4Wo.js +0 -8
- package/packages/ui/dist/assets/chevron-right-BQuQk5zc.js +0 -8
- package/packages/ui/dist/assets/chevron-top-1oeSXkx4.js +0 -8
- package/packages/ui/dist/assets/core-DbkMGSYW.js +0 -907
- package/packages/ui/dist/assets/cursor-DRFqL8nZ.js +0 -3
- package/packages/ui/dist/assets/events-CZkVDFih.js +0 -1
- package/packages/ui/dist/assets/external-link-CX8skCbH.js +0 -8
- package/packages/ui/dist/assets/fallback-DWQYKf3l.js +0 -1
- package/packages/ui/dist/assets/index-BP-UuQ8g.js +0 -1
- package/packages/ui/dist/assets/index-BksbyFjf.js +0 -1
- package/packages/ui/dist/assets/index-C55-Ne92.js +0 -16
- package/packages/ui/dist/assets/index-D6_Zei-R.js +0 -1790
- package/packages/ui/dist/assets/index-DZ2C2rHq.css +0 -1
- package/packages/ui/dist/assets/index-uSdXE5b_.js +0 -1
- package/packages/ui/dist/assets/index.es-dZlNPpfR.js +0 -26
- package/packages/ui/dist/assets/metamask-sdk-RbLbxUgF.js +0 -542
- package/packages/ui/dist/assets/native-CJ5et6AR.js +0 -1
- package/packages/ui/dist/assets/parseSignature-E29x8poM.js +0 -1
- package/scripts/check-docs.mjs +0 -309
- package/scripts/check-init.mjs +0 -123
- package/scripts/check-update.mjs +0 -177
- package/scripts/clean.mjs +0 -17
- package/scripts/pack.mjs +0 -21
- /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
- /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/README.md +0 -0
- /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
- /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sail-token-resolve
|
|
3
|
+
description: Resolve the tokens a user names by symbol or address into on-chain metadata (address, decimals) AND a cross-chain, cross-DEX liquidity map — which Sail chain and which protocol (Uniswap V3/V4, Sushiswap, PancakeSwap, Aerodrome…) actually hold liquidity, how deep each pool is, and whether the leg is swap-ready. Use BEFORE building any swap/DCA/LP/lending mandate, or whenever a user says "a portfolio of X and Y" / "DCA into A, B, C" / "can I swap X here?". Pass every symbol at once. Resolves ANY symbol — curated registry → keyless GeckoTerminal DEX index → on-chain symbol()+decimals() — and recommends which chain to act on. Runs the bundled `scripts/resolve-token.mjs` (no dependencies, no gas, no API key).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sail-token-resolve — tokens → addresses + where the liquidity lives
|
|
7
|
+
|
|
8
|
+
Users name tokens by symbol ("WETH", "UNI", "HYPE", "MORPHO") far more often than by
|
|
9
|
+
address, and they think in *portfolios* ("a DCA of USDC, UNI and MORPHO"), not single
|
|
10
|
+
legs. This skill takes **one or many** symbols and, for each, returns: the verified
|
|
11
|
+
on-chain **address + decimals** per chain, a **liquidity venue map** (which chain, which
|
|
12
|
+
DEX/protocol, which pool, how deep), whether it is **swap-ready**, and a **recommendation**
|
|
13
|
+
when there's a discrepancy — liquidity split across chains, no pool on your chain but a deep
|
|
14
|
+
one elsewhere, or no pool on any Sail chain at all.
|
|
15
|
+
|
|
16
|
+
**"token exists" ≠ "token is swap-ready."** A token can have a valid contract with zero
|
|
17
|
+
routable liquidity. Swap-ready (on-chain confirmed) means Uniswap V3 QuoterV2 returns a
|
|
18
|
+
non-zero USDC→token quote — Sail's executable fast-path route.
|
|
19
|
+
|
|
20
|
+
## When to load
|
|
21
|
+
|
|
22
|
+
- The user names one or more tokens and you need addresses/decimals for a mandate.
|
|
23
|
+
- A user describes a **portfolio / DCA / basket** — resolve every symbol in one call.
|
|
24
|
+
- Before `sail-swap-quote` or `sail-template-swap` — both consume this skill's output.
|
|
25
|
+
- Whenever the user asks "can I swap X here?", "where's the best liquidity for X?", or
|
|
26
|
+
"which chain should I use for this strategy?"
|
|
27
|
+
|
|
28
|
+
## Run it
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# From the project root (reads .sail/.env.local for RPCs + chain):
|
|
32
|
+
node scripts/resolve-token.mjs WETH # single token, configured chain(s)
|
|
33
|
+
node scripts/resolve-token.mjs LINK --chain unichain # force one chain
|
|
34
|
+
node scripts/resolve-token.mjs 0x4200…0006 --chain base # address input
|
|
35
|
+
node scripts/resolve-token.mjs USDC UNI HYPE MORPHO # PORTFOLIO → rich JSON
|
|
36
|
+
node scripts/resolve-token.mjs UNI --all-chains --json # scan every Sail mainnet
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Flags: `--chain <ethereum|unichain|base|arbitrum>` forces one chain; `--rpc <url>` overrides
|
|
40
|
+
the endpoint; `--all-chains` maps every Sail mainnet (even ones without an RPC configured —
|
|
41
|
+
those use GeckoTerminal-only data) so you can recommend "put your SMA on chain Y"; `--json`
|
|
42
|
+
forces the rich per-token map for a single token.
|
|
43
|
+
|
|
44
|
+
## Output shapes (pick the right consumer)
|
|
45
|
+
|
|
46
|
+
| Invocation | stdout shape |
|
|
47
|
+
|---|---|
|
|
48
|
+
| 1 symbol, single configured chain / `--chain` | **bare object** — `{address, decimals, feeTier, swapReady, quote, venues[], …}` (the shape `sail-swap-quote` / `sail-template-swap` expect) |
|
|
49
|
+
| 1 symbol, ≥2 configured chains | **array** of bare objects (one per chain) |
|
|
50
|
+
| ≥2 symbols | **portfolio**: `{ tokens: [tokenWrapper…], summary }` |
|
|
51
|
+
| 1 symbol + `--json`/`--all-chains` | **token wrapper** (see below) |
|
|
52
|
+
|
|
53
|
+
A **token wrapper** is:
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
{
|
|
57
|
+
"query": "UNI",
|
|
58
|
+
"chains": { // keyed by chain name; each value is a bare per-chain object
|
|
59
|
+
"base": { "address": "0x…", "decimals": 18, "swapReady": true, "feeTier": 10000,
|
|
60
|
+
"venues": [ … ], "bestVenue": { … }, "onchainVerified": true, … },
|
|
61
|
+
"unichain": { … }
|
|
62
|
+
},
|
|
63
|
+
"chainsWithLiquidity": ["unichain","base","arbitrum"],
|
|
64
|
+
"onSailChain": true,
|
|
65
|
+
"crossChain": { "action": "route", "deepestChain": "base", "note": "…human guidance…" }
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A **venue** (inside `venues[]`, sorted deepest-first, capped at 8; `venuesTotal` is the full
|
|
70
|
+
count) is:
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
{ "protocol": "uniswap-v3", // uniswap-v3 | uniswap-v4 | uniswap-v2 | sushiswap | pancakeswap | aerodrome | other
|
|
74
|
+
"dexId": "uniswap-v3-base", // raw GeckoTerminal id
|
|
75
|
+
"pool": "0x…", "feeTier": 500, // basis points (500 = 0.05%); null if the pool has no fee in its name
|
|
76
|
+
"pairedSymbol": "USDC", "pairedToken": "0x…",
|
|
77
|
+
"liquidityUsd": 8645941, "volume24hUsd": 22333927,
|
|
78
|
+
"sailRoutable": true, // Sail's fast path can route it (see below)
|
|
79
|
+
"quoteVerified": true } // a live on-chain QuoterV2 quote confirmed THIS venue
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
1. **Symbol → address** (two layers): curated registry (instant, offline) → GeckoTerminal
|
|
85
|
+
search (keyless CoinGecko DEX index) for anything not curated. Candidates are ranked by
|
|
86
|
+
pool liquidity; the on-chain `symbol()` check is the final authority (a wrong DEX-side
|
|
87
|
+
match is rejected, not trusted).
|
|
88
|
+
2. **On-chain verify** — `symbol()` + `decimals()` via eth_call on every chain that has an
|
|
89
|
+
RPC. This is the source of truth (`decimalsSource: "onchain"`). On an `--all-chains` scan
|
|
90
|
+
of a chain with no RPC, metadata falls back to GeckoTerminal (`decimalsSource:
|
|
91
|
+
"geckoterminal-unverified"`) — flag this to the user before wiring it into a mandate.
|
|
92
|
+
3. **Liquidity venue map** — `GET /networks/{net}/tokens/{addr}/pools` returns every pool
|
|
93
|
+
GeckoTerminal indexes, across **all DEXes** (Uniswap V3/V4, Sushiswap, PancakeSwap,
|
|
94
|
+
Aerodrome, …), with the protocol, pool address, fee tier and USD depth. One call per token
|
|
95
|
+
per chain.
|
|
96
|
+
4. **Swap-readiness (on-chain confirmed)** — quotes USDC→token across fee tiers 500/3000/10000
|
|
97
|
+
via Uniswap V3 QuoterV2 and marks the matching venue `quoteVerified: true`. This is the only
|
|
98
|
+
*executable* signal; everything else in `venues[]` is informational.
|
|
99
|
+
|
|
100
|
+
## `sailRoutable` — what Sail's fast path can actually swap
|
|
101
|
+
|
|
102
|
+
Sail's `sail-template-swap` fast path routes through **Uniswap V3** (everywhere) and the
|
|
103
|
+
**Uniswap V4** Universal Router (on Unichain). Those venues are marked `sailRoutable: true`.
|
|
104
|
+
Sushiswap, PancakeSwap, Aerodrome (and Uniswap V2) are detected and surfaced so you can see
|
|
105
|
+
*where the liquidity really is*, but they're `sailRoutable: false` — Sail can't route them via
|
|
106
|
+
the fast path. If the only liquidity is on a non-routable DEX, the token needs a custom mandate
|
|
107
|
+
(`sail-mandates`) or should be held. (Pools with absurd fees — >10% — are spam and are never
|
|
108
|
+
marked routable.)
|
|
109
|
+
|
|
110
|
+
## How to present results to the user
|
|
111
|
+
|
|
112
|
+
Read `crossChain.action` (per token) and the portfolio `summary`, then advise:
|
|
113
|
+
|
|
114
|
+
- **`route`** — swap-ready on a configured chain. If it's routable on **more than one**
|
|
115
|
+
configured chain, surface both with their depths and ask which to use (or pick by where the
|
|
116
|
+
rest of the basket lives). Hand the chosen chain's bare object to `sail-swap-quote`.
|
|
117
|
+
- **`suggest-sma`** — no routable pool on the configured chain(s), but a deep one on another
|
|
118
|
+
Sail chain. Tell the user and **recommend deploying an SMA on that chain** for this leg
|
|
119
|
+
(e.g. "MORPHO has no Uniswap pool on Base; the deep pool is Uniswap V3 on Unichain — consider
|
|
120
|
+
an SMA on Unichain"). Don't silently drop it.
|
|
121
|
+
- **`manual-address`** — liquidity exists but only on a DEX Sail can't fast-route (e.g. only on
|
|
122
|
+
Aerodrome). Offer a custom mandate via `sail-mandates`, or hold the leg.
|
|
123
|
+
- **`hold-skip`** — no pool on any scanned Sail chain (the token may live on a non-Sail chain,
|
|
124
|
+
e.g. a HyperEVM-only asset). Recommend holding/dropping it from the strategy.
|
|
125
|
+
|
|
126
|
+
Always show the resolved **address + decimals + the chain/protocol/depth** you're acting on, so
|
|
127
|
+
the user can sanity-check before anything is signed.
|
|
128
|
+
|
|
129
|
+
## Worked example — "create a DCA strategy of USDC, UNI, HYPE and MORPHO"
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node scripts/resolve-token.mjs USDC UNI HYPE MORPHO --all-chains
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A typical read of the result:
|
|
136
|
+
- **USDC** — the quote asset; swap-ready everywhere (`feeTier: null`, it *is* USDC).
|
|
137
|
+
- **UNI** — `route`; routable on Unichain/Base/Arbitrum. Pick by depth or by where the basket
|
|
138
|
+
concentrates.
|
|
139
|
+
- **HYPE** — `route` only on **Unichain** (a real ~$4M Uniswap V3 USDC pool); absent/illiquid
|
|
140
|
+
elsewhere. Tell the user HYPE is a Unichain-only leg here.
|
|
141
|
+
- **MORPHO** — `route` on **Base** (Uniswap V3 USDC pool); on other chains it exists but has no
|
|
142
|
+
routable USDC pool. If the project is configured for Base, good; if not, `suggest-sma`.
|
|
143
|
+
|
|
144
|
+
Then, for each `route`/chosen-chain leg, hand `(address, decimals, feeTier)` to
|
|
145
|
+
[`sail-swap-quote`](../sail-swap-quote/SKILL.md) → [`sail-template-swap`](../sail-template-swap/SKILL.md).
|
|
146
|
+
|
|
147
|
+
## Important
|
|
148
|
+
|
|
149
|
+
- **Decimals are critical** — 25 USDC = `25_000_000` (6 dec); 1 WETH =
|
|
150
|
+
`1_000_000_000_000_000_000` (18 dec). Every cap in a mandate is base units. Trust
|
|
151
|
+
`decimalsSource: "onchain"`; treat `geckoterminal-unverified` as provisional.
|
|
152
|
+
- **Addresses are per-chain** — WETH on Unichain ≠ WETH on Base ≠ WETH on Arbitrum. Resolve and
|
|
153
|
+
verify separately per chain; never copy an address across chains.
|
|
154
|
+
- **`getPool` is unreliable** on some forks — swap-readiness trusts a non-zero QuoterV2 quote,
|
|
155
|
+
never the V3 factory's `getPool`.
|
|
156
|
+
- **Symbol ambiguity / scam collisions.** When a symbol isn't curated, the deepest-pool
|
|
157
|
+
candidate whose on-chain `symbol()` matches is used. A scam token can share a real symbol; the
|
|
158
|
+
liquidity ranking favours the canonical one, but for an obscure symbol glance at the resolved
|
|
159
|
+
`address` and `source` before wiring it in — or pass the intended contract's `0x` address
|
|
160
|
+
directly (address-input bypasses the DEX lookup). Also sanity-check a venue's `volume24hUsd`
|
|
161
|
+
against its `liquidityUsd`: a huge pool with near-zero volume is inflated/non-trading, and a
|
|
162
|
+
big pool whose `pairedSymbol` is a look-alike (e.g. a "HYPE/BASEDHYPE" pool) is a *different*
|
|
163
|
+
asset — depth there does not make *your* token swap-ready.
|
|
164
|
+
- **`bestVenue` is USDC-relevant, not the biggest pool.** It's the deepest *Sail-routable,
|
|
165
|
+
USDC-paired* pool (the one a USDC DCA would route through), so it can be smaller than the
|
|
166
|
+
largest pool in `venues[]` (which may be a WETH or look-alike pair).
|
|
167
|
+
- **Missing venues ≠ no liquidity.** A chain can be `swapReady: true` (from the on-chain
|
|
168
|
+
QuoterV2 probe) yet show empty `venues[]` with `venuesError` set if GeckoTerminal rate-limited
|
|
169
|
+
that call. Trust `swapReady`; to repopulate the venue map for one chain, re-run
|
|
170
|
+
`node scripts/resolve-token.mjs <SYM> --chain <name> --json`.
|
|
171
|
+
- **Rate limits** — GeckoTerminal is keyless and rate-limited; the script throttles and caches
|
|
172
|
+
calls. A big `--all-chains` portfolio takes a moment. Set `GECKO_MIN_SPACING_MS` to tune the
|
|
173
|
+
spacing. A transient GeckoTerminal failure on one chain sets `venuesError` and leaves
|
|
174
|
+
`swapReady` intact (it's from the on-chain probe) — it never aborts the whole run.
|
|
@@ -18,3 +18,12 @@ CHAIN_ID=8453
|
|
|
18
18
|
|
|
19
19
|
# Optional: non-interactive passphrase (CI, GitHub Actions, launchd, systemd)
|
|
20
20
|
# SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
|
|
21
|
+
|
|
22
|
+
# Advanced — exposing the dashboard/API beyond localhost.
|
|
23
|
+
# The UI server binds 127.0.0.1 (local only) by default. Its key-management
|
|
24
|
+
# endpoints are UNAUTHENTICATED, so only expose them behind your own auth
|
|
25
|
+
# (a reverse proxy with basic-auth, or a private tailnet — never the open internet).
|
|
26
|
+
# SAILOR_HOST=0.0.0.0 # bind all interfaces (LAN / behind a domain)
|
|
27
|
+
# SAILOR_TRUST_PROXY=1 # hop count (or IP allowlist) when behind a reverse proxy, so the rate limit keys on the real client IP (else it's a single global bucket)
|
|
28
|
+
# SAILOR_CORS_ORIGINS=https://sail.example.ts.net # extra allowed browser origins (comma-separated)
|
|
29
|
+
# SAILOR_RATE_LIMIT_PER_MIN=100 # throttle /api/signer + /api/onboard/generate-key (default 100; or set "rateLimitPerMin" in .sail/config.json)
|
|
@@ -40,7 +40,7 @@ During **setup**, always ask before anything that costs gas. Once the **mandate
|
|
|
40
40
|
|
|
41
41
|
## First contact
|
|
42
42
|
|
|
43
|
-
When the user says start (or any first message), present the welcome above in full — definition, stage list, handoff line — before doing anything else. Do not launch the UI yet. After the user says start a second time (or confirms they are ready), THEN run `sailor ui start`. The welcome and the
|
|
43
|
+
When the user says start (or any first message), present the welcome above in full — definition, stage list, handoff line — before doing anything else. Do not launch the UI yet. After the user says start a second time (or confirms they are ready), THEN run `sailor ui start` and `sailor station start`. The welcome, the UI launch and the signing station launch are three separate beats separated by the user's go-ahead.
|
|
44
44
|
|
|
45
45
|
If the user's first message is an npm install command, run it, then present the welcome immediately after it completes — do not wait for another message.
|
|
46
46
|
|
|
@@ -80,11 +80,28 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
|
|
|
80
80
|
| sail-onboarding | New project setup, or resuming a partially set-up project, documentation of sailor commands | `.agents/skills/sail-onboarding/SKILL.md` |
|
|
81
81
|
| sail-project-info | Any question about project, account, mandate, chain, or environment state | `.agents/skills/sail-project-info/SKILL.md` |
|
|
82
82
|
| sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/SKILL.md` |
|
|
83
|
+
| sail-token-resolve | Resolving a token symbol/address to its on-chain address + decimals, and checking whether a Uniswap V3 pool exists (swap-readiness) | `.agents/skills/sail-token-resolve/SKILL.md` |
|
|
84
|
+
| sail-swap-quote | Fetching a live Uniswap V3 quote and the slippage-adjusted amountOutMinimum floor | `.agents/skills/sail-swap-quote/SKILL.md` |
|
|
85
|
+
| sail-templates | Registry + reuse guide for Sail's shared permission singletons — which primitives exist as templates, per-chain deployment status (`deployed.json`), and the register→configure reuse flow. Start here before any template mandate | `.agents/skills/sail-templates/SKILL.md` |
|
|
86
|
+
| sail-template-swap | Bounded DEX swap / DCA mandate via the shared SwapPermission singleton (oracle-gated slippage band) — register + configure, no Solidity. The fast path for a swap/DCA strategy | `.agents/skills/sail-template-swap/SKILL.md` |
|
|
87
|
+
| sail-template-swap-no-oracle | Bounded swap for tokens with NO oracle via SwapPermissionNoOracle (single-pool hallucination guard, NOT manipulation-resistant) — reference-only, not yet deployed on any chain | `.agents/skills/sail-template-swap-no-oracle/SKILL.md` |
|
|
88
|
+
| sail-template-transfer | Bounded ERC-20 transfer to a mutable recipient allowlist via TransferPermission | `.agents/skills/sail-template-transfer/SKILL.md` |
|
|
89
|
+
| sail-template-withdraw | Bounded ERC-20 withdraw to ONE fixed recipient (owner-Safe consolidation) via WithdrawPermission | `.agents/skills/sail-template-withdraw/SKILL.md` |
|
|
90
|
+
| sail-template-deposit | Bounded deposit into ERC-4626 vaults / Aave v2–v3 via DepositPermission | `.agents/skills/sail-template-deposit/SKILL.md` |
|
|
91
|
+
| sail-template-borrow | Bounded lending borrow against Aave / Morpho / Compound with an on-chain LTV check via BorrowPermission | `.agents/skills/sail-template-borrow/SKILL.md` |
|
|
92
|
+
| sail-template-approve-batch | Atomic approve → call → reset (allowance bracket) in one batch via ApproveAndCallBatchPermission | `.agents/skills/sail-template-approve-batch/SKILL.md` |
|
|
83
93
|
| sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
|
|
84
|
-
| sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts | `.agents/skills/sail-mandates/SKILL.md` |
|
|
94
|
+
| sail-mandates | Designing, authoring, testing, deploying, or authorizing permission contracts (the bespoke-Solidity escape hatch) | `.agents/skills/sail-mandates/SKILL.md` |
|
|
85
95
|
| sail-automation | Automating the agent — GitHub Actions, self-hosted runner, Docker, or local daemon | `.agents/skills/sail-automation/SKILL.md` |
|
|
86
96
|
| sail-extend | Notifications or a custom dashboard, once the agent is live | `.agents/skills/sail-extend/SKILL.md` |
|
|
87
97
|
|
|
98
|
+
**Shared singletons are the default for the common DeFi primitives.** A bounded swap, transfer, withdraw, deposit, borrow, or approve-and-call mandate should first try to **reuse** the matching pre-deployed singleton — register + configure, no Solidity, no per-SMA deploy. Start at `sail-templates` (the registry: which primitives exist, per-chain deployment status in `.agents/skills/sail-templates/deployed.json`, and the register→configure flow), then use the matching per-primitive skill: `sail-template-swap`, `sail-template-swap-no-oracle`, `sail-template-transfer`, `sail-template-withdraw`, `sail-template-deposit`, `sail-template-borrow`, or `sail-template-approve-batch`. Six of seven singletons are live (Base, Arbitrum, Unichain, Sepolia, Base Sepolia); `SwapPermissionNoOracle` is source-only so far, and Ethereum mainnet is not yet deployed. Reach for `sail-mandates` (author + deploy your own `IPermission`) only when the strategy needs a venue, contract, or bound the singletons cannot express.
|
|
99
|
+
|
|
100
|
+
**Bundled scripts** under `scripts/` are dependency-free tools that make the fast path quick and deterministic — run them from the project root so they read `.sail/.env.local`:
|
|
101
|
+
- `resolve-token.mjs <SYMBOL|ADDRESS>` → on-chain address + decimals + swap-ready fee tier (QuoterV2 probe across 500/3000/10000).
|
|
102
|
+
- `quote-swap.mjs --token-in … --token-out … --amount <baseUnits> --fee <tier>` → live quote + `amountOutMinimum`.
|
|
103
|
+
- `shared-template-addr.mjs <TemplateName>` → the singleton's deployed address on the active chain.
|
|
104
|
+
|
|
88
105
|
## Invariants — apply to every turn
|
|
89
106
|
|
|
90
107
|
- Do not present the welcome and immediately launch the UI — wait for the second "start"
|
|
@@ -97,5 +114,7 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
|
|
|
97
114
|
- Do not commit `SAIL_PASSPHRASE` or private keys
|
|
98
115
|
- ERC-20 `approve()` calls are NOT covered by supply, swap, or deposit permissions — every approve the strategy makes needs explicit coverage. Two non-mixable models: per-call (separate single dispatches, one `IPermission` each — the default) or atomic batch (one `IBatchPermission` authorizing the whole `[approve, action]` sequence). A normal `IPermission` cannot authorize a batch. Details: `.agents/skills/sail-mandates/references/approvals.md`
|
|
99
116
|
- Never authorize (attach) a permission before `forge test` and `sailor mandate simulate` both pass against samples derived from the user's strategy
|
|
117
|
+
- **Register ≠ configure for shared singletons.** `sailor mandate attach` only registers the address on the kernel (`isConfigured` stays `false`); the kernel denies every call until you also run `sailor mandate configure`. Stopping at `attach` is the most common trap. See `sail-templates` (reuse-flow) and `sail-template-swap`.
|
|
118
|
+
- **Resolve tokens before binding them.** Never guess a token address or assume a token that exists is swap-ready. Run `scripts/resolve-token.mjs` for symbol→address+decimals+swap-readiness; a decimals mismatch (USDC 6 vs most 18) silently mis-sizes every cap, and a token with no V3 pool will fail-closed on every dispatch. Addresses are per-chain — never copy one across chains.
|
|
100
119
|
- Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
|
|
101
120
|
- Operator intent and the strategy's stated bounds outrank any example. If the operator asks for a bound an example omits, include it. Never let an example's shape narrow a mandate below what the operator requested
|
|
@@ -89,5 +89,5 @@ Run `sailor doctor` (read-only, no gas, no keys):
|
|
|
89
89
|
- on a conjunctive kernel, **probes each permission for pass-through** and flags any
|
|
90
90
|
that would brick dispatch.
|
|
91
91
|
|
|
92
|
-
See [
|
|
93
|
-
revert failure-mode catalog.
|
|
92
|
+
See [AGENTS.md](../AGENTS.md) (and the skills it routes to under `.agents/skills/`)
|
|
93
|
+
for the operational decision tree and the revert failure-mode catalog.
|
|
@@ -10,6 +10,10 @@ struct Context {
|
|
|
10
10
|
uint256 value;
|
|
11
11
|
uint256 blockTimestamp;
|
|
12
12
|
uint256 blockNumber;
|
|
13
|
+
/// @dev Kernel registrationEpoch(account, permission) at dispatch. Configurable
|
|
14
|
+
/// permissions fail closed on a mismatch with the epoch stamped at configure()
|
|
15
|
+
/// time; permissions without post-deploy configuration can ignore it.
|
|
16
|
+
uint256 configEpoch;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
interface IPermission {
|
|
@@ -36,11 +36,14 @@ struct Context {
|
|
|
36
36
|
uint256 value; // msg.value (native asset) sent with the call
|
|
37
37
|
uint256 blockTimestamp; // block.timestamp at evaluation
|
|
38
38
|
uint256 blockNumber; // block.number at evaluation
|
|
39
|
+
uint256 configEpoch; // kernel registrationEpoch(account, permission) at dispatch;
|
|
40
|
+
// ignore it unless your permission takes post-deploy configuration
|
|
39
41
|
}
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
- `evaluate` — your policy. Return `true` to permit the call, `false` to block it. Runs under a
|
|
43
|
-
|
|
45
|
+
150,000-gas `staticcall` (`SailKernel.PERMISSION_GAS_CAP`); a revert or gas overage is treated
|
|
46
|
+
as `false`.
|
|
44
47
|
- `discriminator` — a stable `bytes32` name for your permission (e.g. `keccak256("MyMandate")`).
|
|
45
48
|
|
|
46
49
|
Keep all policy parameters constructor-configured so each deployment is a complete, reviewable
|
|
@@ -26,6 +26,8 @@ struct BatchContext {
|
|
|
26
26
|
bytes32 batchHash; // keccak256(abi.encode(calls)) — stable id for the exact sequence
|
|
27
27
|
uint256 blockTimestamp; // block.timestamp at dispatch
|
|
28
28
|
uint256 blockNumber; // block.number at dispatch
|
|
29
|
+
uint256 configEpoch; // kernel registrationEpoch(account, permission) at dispatch —
|
|
30
|
+
// same fail-closed freshness check as Context.configEpoch
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
interface IBatchPermission {
|
|
@@ -10,6 +10,10 @@ struct Context {
|
|
|
10
10
|
uint256 value;
|
|
11
11
|
uint256 blockTimestamp;
|
|
12
12
|
uint256 blockNumber;
|
|
13
|
+
/// @dev Kernel registrationEpoch(account, permission) at dispatch. Configurable
|
|
14
|
+
/// permissions fail closed on a mismatch with the epoch stamped at configure()
|
|
15
|
+
/// time; permissions without post-deploy configuration can ignore it.
|
|
16
|
+
uint256 configEpoch;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
interface IPermission {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// quote-swap.mjs — live Uniswap V3 quote via QuoterV2 + an amountOutMinimum floor.
|
|
3
|
+
//
|
|
4
|
+
// Takes resolved tokens + an exact amount (from resolve-token.mjs), returns the
|
|
5
|
+
// expected output and a slippage-adjusted amountOutMinimum ready to embed in a
|
|
6
|
+
// swap dispatch. Pure JS, no dependencies.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/quote-swap.mjs \
|
|
9
|
+
// --token-in 0x078D...AD6 --decimals-in 6 \
|
|
10
|
+
// --token-out 0x4200...0006 --decimals-out 18 \
|
|
11
|
+
// --amount 25000000 --fee 3000 --slippage-bps 100
|
|
12
|
+
//
|
|
13
|
+
// Output: one JSON object on stdout; human notes on stderr.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { resolve as resolvePath } from "node:path";
|
|
17
|
+
|
|
18
|
+
const CHAINS = {
|
|
19
|
+
ethereum: { chainId: 1, quoterV2: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" },
|
|
20
|
+
unichain: { chainId: 130, quoterV2: "0x385a5cf5f83e99f7bb2852b6a19c3538b9fa7658" },
|
|
21
|
+
base: { chainId: 8453, quoterV2: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a" },
|
|
22
|
+
arbitrum: { chainId: 42161, quoterV2: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SEL_QUOTE = "0xc6a5026a"; // quoteExactInputSingle((address,address,uint256,uint24,uint160))
|
|
26
|
+
const ADDR_ZERO = "0x" + "0".repeat(40);
|
|
27
|
+
|
|
28
|
+
// ── minimal ABI encode/decode (mirrors resolve-token.mjs) ───────────────────────
|
|
29
|
+
function pad32(h) {
|
|
30
|
+
h = h.toLowerCase().replace(/^0x/, "");
|
|
31
|
+
if (h.length < 64) h = "0".repeat(64 - h.length) + h;
|
|
32
|
+
return h;
|
|
33
|
+
}
|
|
34
|
+
function uintToHex(n) {
|
|
35
|
+
let h = n.toString(16);
|
|
36
|
+
if (h.length % 2) h = "0" + h;
|
|
37
|
+
return pad32(h);
|
|
38
|
+
}
|
|
39
|
+
function encodeQuoteCall(tokenIn, tokenOut, amountIn, fee) {
|
|
40
|
+
return SEL_QUOTE + pad32(tokenIn) + pad32(tokenOut) + uintToHex(amountIn) + uintToHex(BigInt(fee)) + uintToHex(0n);
|
|
41
|
+
}
|
|
42
|
+
function decodeUint(hex, word = 0) {
|
|
43
|
+
const h = hex.toLowerCase().replace(/^0x/, "");
|
|
44
|
+
const w = h.slice(word * 64, word * 64 + 64);
|
|
45
|
+
return w ? BigInt("0x" + w) : 0n;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function ethCall(rpc, to, data, from = ADDR_ZERO) {
|
|
49
|
+
const res = await fetch(rpc, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to, data, from }, "latest"] }),
|
|
53
|
+
signal: AbortSignal.timeout(15_000),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) throw new Error(`eth_call HTTP ${res.status}`);
|
|
56
|
+
const json = await res.json();
|
|
57
|
+
if (json.error) throw new Error(`QuoterV2 reverted: ${JSON.stringify(json.error)}`);
|
|
58
|
+
if (!json.result) throw new Error("eth_call returned no result");
|
|
59
|
+
return json.result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readSailEnv() {
|
|
63
|
+
const p = resolvePath(process.cwd(), ".sail", ".env.local");
|
|
64
|
+
const out = {};
|
|
65
|
+
if (existsSync(p)) {
|
|
66
|
+
for (const line of readFileSync(p, "utf8").split("\n")) {
|
|
67
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
68
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function readSailConfig() {
|
|
74
|
+
const p = resolvePath(process.cwd(), ".sail", "config.json");
|
|
75
|
+
if (!existsSync(p)) return {};
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
78
|
+
} catch {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function resolveChain(chainFlag) {
|
|
83
|
+
if (chainFlag) {
|
|
84
|
+
const c = CHAINS[chainFlag.toLowerCase()];
|
|
85
|
+
if (c) return { name: chainFlag.toLowerCase(), ...c };
|
|
86
|
+
for (const [name, cfg] of Object.entries(CHAINS)) if (String(cfg.chainId) === String(chainFlag)) return { name, ...cfg };
|
|
87
|
+
throw new Error(`Unknown chain "${chainFlag}". Known: ${Object.keys(CHAINS).join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
const env = readSailEnv();
|
|
90
|
+
const cfg = readSailConfig();
|
|
91
|
+
const id = env.CHAIN_ID ?? cfg.chainId;
|
|
92
|
+
if (id) for (const [name, c] of Object.entries(CHAINS)) if (String(c.chainId) === String(id)) return { name, ...c };
|
|
93
|
+
throw new Error(`Could not resolve chain. Pass --chain <${Object.keys(CHAINS).join("|")}>.`);
|
|
94
|
+
}
|
|
95
|
+
function resolveRpc(chain, rpcFlag) {
|
|
96
|
+
if (rpcFlag) return rpcFlag;
|
|
97
|
+
const env = readSailEnv();
|
|
98
|
+
// Mirror resolve-token.mjs / packages/cli getRpcUrl(): named var, then chainId-keyed
|
|
99
|
+
// var (RPC_URL_8453 — written by the UI / used by multi-chain projects), then generic.
|
|
100
|
+
const nameVar = `${chain.name.toUpperCase().replace("-", "_")}_RPC_URL`;
|
|
101
|
+
const idVar = `RPC_URL_${chain.chainId}`;
|
|
102
|
+
return env[nameVar] ?? env[idVar] ?? env.RPC_URL ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseArgs(argv) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
const a = argv[i];
|
|
109
|
+
if (a.startsWith("--")) {
|
|
110
|
+
const key = a.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
111
|
+
out[key] = argv[++i];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function fmtUnits(valueBI, decimals) {
|
|
118
|
+
// bigint base units → human string, preserving precision
|
|
119
|
+
const neg = valueBI < 0n;
|
|
120
|
+
let s = (neg ? -valueBI : valueBI).toString().padStart(decimals + 1, "0");
|
|
121
|
+
const intPart = s.slice(0, s.length - decimals);
|
|
122
|
+
const fracPart = decimals > 0 ? s.slice(s.length - decimals) : "";
|
|
123
|
+
const trimmedFrac = fracPart.replace(/0+$/, "");
|
|
124
|
+
let out = trimmedFrac.length ? `${intPart}.${trimmedFrac}` : intPart;
|
|
125
|
+
if (decimals > 0 && !trimmedFrac) out = `${intPart}.0`;
|
|
126
|
+
return (neg ? "-" : "") + out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function errMsg(e) {
|
|
130
|
+
return e && typeof e.message === "string" ? e.message : String(e);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
const a = parseArgs(process.argv.slice(2));
|
|
135
|
+
if (process.argv.length <= 2 || a.help) {
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
"Usage: quote-swap.mjs --token-in <addr> --token-out <addr> --amount <baseUnits>\n" +
|
|
138
|
+
" --decimals-in <n> --decimals-out <n> --fee <tier> [--slippage-bps <n>] [--chain <>] [--rpc URL]\n",
|
|
139
|
+
);
|
|
140
|
+
process.exit(process.argv.length <= 2 ? 1 : 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const need = ["tokenIn", "tokenOut", "amount", "decimalsIn", "decimalsOut", "fee"];
|
|
144
|
+
for (const k of need) if (a[k] === undefined) throw new Error(`Missing --${k.replace(/([A-Z])/g, "-$1").toLowerCase()}`);
|
|
145
|
+
|
|
146
|
+
const chain = resolveChain(a.chain);
|
|
147
|
+
const rpc = resolveRpc(chain, a.rpc);
|
|
148
|
+
if (!rpc) throw new Error(`No RPC for ${chain.name}. Pass --rpc or set RPC_URL in .sail/.env.local.`);
|
|
149
|
+
|
|
150
|
+
const tokenIn = a.tokenIn;
|
|
151
|
+
const tokenOut = a.tokenOut;
|
|
152
|
+
const amountIn = BigInt(a.amount);
|
|
153
|
+
const fee = Number(a.fee);
|
|
154
|
+
const decimalsIn = Number(a.decimalsIn);
|
|
155
|
+
const decimalsOut = Number(a.decimalsOut);
|
|
156
|
+
const slippageBps = a.slippageBps !== undefined ? Number(a.slippageBps) : 100; // default 1%
|
|
157
|
+
if (!Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > 10_000) {
|
|
158
|
+
throw new Error(`--slippage-bps must be an integer in [0, 10000], got "${a.slippageBps}".`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = encodeQuoteCall(tokenIn, tokenOut, amountIn, fee);
|
|
162
|
+
const ret = await ethCall(rpc, chain.quoterV2, data);
|
|
163
|
+
const amountOut = decodeUint(ret, 0); // word 0
|
|
164
|
+
const gasEstimate = decodeUint(ret, 3); // word 3
|
|
165
|
+
|
|
166
|
+
if (amountOut === 0n) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`QuoterV2 returned 0 for ${amountIn} (tokenIn) → tokenOut at fee ${fee}. The pool is empty or does not exist at this tier.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const amountOutMinimum = (amountOut * BigInt(10_000 - slippageBps)) / 10_000n;
|
|
173
|
+
// price = (amountOut / 10^decOut) / (amountIn / 10^decIn) → units of tokenOut per tokenIn
|
|
174
|
+
const humanIn = fmtUnits(amountIn, decimalsIn);
|
|
175
|
+
const humanOut = fmtUnits(amountOut, decimalsOut);
|
|
176
|
+
const humanMin = fmtUnits(amountOutMinimum, decimalsOut);
|
|
177
|
+
|
|
178
|
+
const out = {
|
|
179
|
+
tokenIn,
|
|
180
|
+
tokenOut,
|
|
181
|
+
decimalsIn,
|
|
182
|
+
decimalsOut,
|
|
183
|
+
amountIn: amountIn.toString(),
|
|
184
|
+
amountOut: amountOut.toString(),
|
|
185
|
+
amountOutMinimum: amountOutMinimum.toString(),
|
|
186
|
+
slippageBps,
|
|
187
|
+
fee,
|
|
188
|
+
chain: chain.name,
|
|
189
|
+
chainId: chain.chainId,
|
|
190
|
+
gasEstimate: gasEstimate.toString(),
|
|
191
|
+
human: {
|
|
192
|
+
amountIn: humanIn,
|
|
193
|
+
amountOut: humanOut,
|
|
194
|
+
amountOutMinimum: humanMin,
|
|
195
|
+
price: `${humanOut} out per ${humanIn} in`,
|
|
196
|
+
},
|
|
197
|
+
note: "amountOutMinimum is the slippage floor — embed it in the swap calldata the agent dispatches.",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
201
|
+
process.stderr.write(
|
|
202
|
+
`\nQuote: ${humanIn} in → ${humanOut} out (fee ${fee})\n` +
|
|
203
|
+
` amountOutMinimum (${slippageBps / 100}% slip): ${humanMin}\n` +
|
|
204
|
+
` gas estimate: ${gasEstimate}\n`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main().catch((err) => {
|
|
209
|
+
process.stderr.write(`\nquote-swap failed: ${errMsg(err)}\n`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
});
|