@dev.sail.money/sailor 0.0.2-21 → 0.0.2-22
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/package.json +1 -1
- package/packages/cli/dist/index.cjs +2347 -2037
- package/packages/cli/dist/server.cjs +110 -140
- package/packages/sdk/dist/deployments.d.ts +14 -7
- package/packages/sdk/dist/deployments.d.ts.map +1 -1
- package/packages/sdk/dist/deployments.js +132 -141
- package/packages/sdk/dist/deployments.js.map +1 -1
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/ui/dist/assets/{add-BzRDG6go.js → add-C--RBwJe.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-C6juL2cm.js → all-wallets-_xwd_eso.js} +1 -1
- package/packages/ui/dist/assets/{app-store-DSJ1ow5G.js → app-store-CIQsK1zU.js} +1 -1
- package/packages/ui/dist/assets/{apple-CUEgIX9k.js → apple-BdlAnnmO.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-BOhBj1Je.js → arrow-bottom-B5p_6Dat.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-C4CzuHiR.js → arrow-bottom-circle-D7c6JPTF.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-BTD8zZ12.js → arrow-left-SA4NpEnP.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-qcOukPwm.js → arrow-right-mOJNWujS.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-C0f2945G.js → arrow-top-CvPVVpHl.js} +1 -1
- package/packages/ui/dist/assets/{bank-BSWzLP3R.js → bank-B2j2rPm9.js} +1 -1
- package/packages/ui/dist/assets/{basic-DGTBsmnG.js → basic-Bw6cXOlk.js} +1 -1
- package/packages/ui/dist/assets/{browser-DJNepafc.js → browser-CUSNF__N.js} +1 -1
- package/packages/ui/dist/assets/{card-u08LJx43.js → card-CpKLox49.js} +1 -1
- package/packages/ui/dist/assets/{ccip-DcWZjG37.js → ccip-XB9iQjXB.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-DVD-obDl.js → checkmark-BRpXeSCK.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-CSXDqIAx.js → checkmark-bold-BkPvoqxo.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-Be7f0gi2.js → chevron-bottom-CtK0W2av.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-v8cgKRhQ.js → chevron-left-NayfPMDy.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-B0ux2X-3.js → chevron-right-BPU2hCfA.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-De-a8tmA.js → chevron-top-CTXwC4nM.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-BIIIRGPA.js → chrome-store-eWIk0-YZ.js} +1 -1
- package/packages/ui/dist/assets/{clock-8d5kvRPQ.js → clock-VmYiq5jB.js} +1 -1
- package/packages/ui/dist/assets/{close-BSSJkFv0.js → close-NfBukMzW.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-CLJaQiUO.js → coinPlaceholder-BWOeJc6j.js} +1 -1
- package/packages/ui/dist/assets/{compass-CR1zP0b-.js → compass-oRk8W3iM.js} +1 -1
- package/packages/ui/dist/assets/{copy-CGkuIFo6.js → copy-GcYQZOsF.js} +1 -1
- package/packages/ui/dist/assets/{core-ea860JM2.js → core-B_rvnvkC.js} +3 -3
- package/packages/ui/dist/assets/cursor-BAViuJWh.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-C8s5LY_P.js → cursor-transparent-CGox3wZ-.js} +1 -1
- package/packages/ui/dist/assets/{desktop-CQyixryE.js → desktop-DU4yyiV4.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-Ct0234l0.js → disconnect-CJm9NnxK.js} +1 -1
- package/packages/ui/dist/assets/{discord-haYPGMDl.js → discord-MxDL8Eq6.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-BTLrS1KK.js → etherscan-CkCvlZiA.js} +1 -1
- package/packages/ui/dist/assets/{events-wdo_D3Zy.js → events-CkyJn32_.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-CMlYpOat.js → exclamation-triangle-hH1JdYAZ.js} +1 -1
- package/packages/ui/dist/assets/{extension-CNGBCbo8.js → extension-DTMrXG5m.js} +1 -1
- package/packages/ui/dist/assets/{external-link-DmYJSKcL.js → external-link-GSwn5MzD.js} +1 -1
- package/packages/ui/dist/assets/{facebook-F0iMVTem.js → facebook-Vw_uyzaE.js} +1 -1
- package/packages/ui/dist/assets/{fallback-BqeFDEuW.js → fallback-BL3U4ZRT.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-CYr9I6UA.js → farcaster-F-_di36M.js} +1 -1
- package/packages/ui/dist/assets/{filters-CxE97nqU.js → filters-DQzcstDl.js} +1 -1
- package/packages/ui/dist/assets/{github-CjRht-Wv.js → github-BSq3_rEd.js} +1 -1
- package/packages/ui/dist/assets/{google-3G0o4pR9.js → google-BU4QXiDS.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-C5gNChqZ.js → help-circle-CuF4iPyF.js} +1 -1
- package/packages/ui/dist/assets/{id-C6_zK0Tb.js → id-BQWlv0a_.js} +1 -1
- package/packages/ui/dist/assets/{image-DsU8Irlu.js → image-BPNySDPo.js} +1 -1
- package/packages/ui/dist/assets/{index-D1lgDFZV.js → index-BMPQOOgv.js} +1 -1
- package/packages/ui/dist/assets/{index-DQ44LBvq.js → index-CMyY4FOR.js} +3 -3
- package/packages/ui/dist/assets/{index-Drc17uEc.js → index-CsbiKM3b.js} +1 -1
- package/packages/ui/dist/assets/{index-CjvcQefO.js → index-D0SPxlSM.js} +1 -1
- package/packages/ui/dist/assets/{index-c8ZmMTds.js → index-D2wgBslE.js} +1 -1
- package/packages/ui/dist/assets/{index-BCb0Nju4.js → index-Dc9_WV0G.js} +76 -76
- package/packages/ui/dist/assets/{index.es-BhDQmlR4.js → index.es-CvyDIsY4.js} +4 -4
- package/packages/ui/dist/assets/{info-CAKKH6T2.js → info-D20yslek.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-CHV1idfy.js → info-circle-BEjvYTHa.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-ew10LUMl.js → lightbulb-DfvLi5mQ.js} +1 -1
- package/packages/ui/dist/assets/{mail-CACYeWXj.js → mail-CkgaIJAd.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-BUYu4RDE.js → metamask-sdk-O-IBvvGq.js} +1 -1
- package/packages/ui/dist/assets/{mobile-DX601q1y.js → mobile-CGc88WfG.js} +1 -1
- package/packages/ui/dist/assets/{more-CQGeX45N.js → more-DnX8wlTn.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-BQw8E4X-.js → network-placeholder-DDrgA4a3.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-CifJ2CzA.js → nftPlaceholder-DhHWPuD3.js} +1 -1
- package/packages/ui/dist/assets/{off-B6oUArCZ.js → off-D1CsYvPQ.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-DjWIdHkx.js → parseSignature-BlZUbtEc.js} +1 -1
- package/packages/ui/dist/assets/{play-store-Kq51oh3r.js → play-store-Dbkk8PTZ.js} +1 -1
- package/packages/ui/dist/assets/{plus-BxZxVpff.js → plus-B8jXpls3.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-CSBFpyhP.js → qr-code-CDuJ3ftj.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-D0HyLkot.js → recycle-horizontal-ZFGjaHsZ.js} +1 -1
- package/packages/ui/dist/assets/{refresh-BAQo228i.js → refresh-D0rMEDtF.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-DeUbwRp6.js → reown-logo-NlCNVmgd.js} +1 -1
- package/packages/ui/dist/assets/{search-C8Sd0Mpz.js → search-CrJAA2qW.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-BujG3JoP.js → secp256k1-mJj6W2AI.js} +1 -1
- package/packages/ui/dist/assets/{send-B0JwUp6Q.js → send-C7CoRziM.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-B2k5yDqc.js → swapHorizontal-fD3wbCGJ.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-yQqq0yPi.js → swapHorizontalBold-Cc-jQ6as.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-NafYmdDj.js → swapHorizontalMedium-DlJW6uX1.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BUn0GwEK.js → swapHorizontalRoundedBold-1VHOerLO.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-Be1m6suD.js → swapVertical-CKaRlkZK.js} +1 -1
- package/packages/ui/dist/assets/{telegram-D-u7QHT5.js → telegram-DnCYed4D.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-B6Y4DFOR.js → three-dots-BFluoxma.js} +1 -1
- package/packages/ui/dist/assets/{twitch-DSy1rhWQ.js → twitch-BXGv98S9.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-tac1plSa.js → twitterIcon-C6IdXEe5.js} +1 -1
- package/packages/ui/dist/assets/{verify-6MylluBY.js → verify-D_QGyiLQ.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-CZc0otb8.js → verify-filled-DIW8QKL9.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-DbT03Pyz.js → w3m-modal-Do9U160p.js} +1 -1
- package/packages/ui/dist/assets/{wallet-473-ObZE.js → wallet-CcARZnOx.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-B9h3WvTk.js → wallet-placeholder-X1coFzQa.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-5GHIf5FR.js → walletconnect-Glte9ia7.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-8A009Dx3.js → warning-circle-j-3V4KTo.js} +1 -1
- package/packages/ui/dist/assets/{x-B4jK8e8X.js → x-Bcc52c_T.js} +1 -1
- package/packages/ui/dist/index.html +1 -1
- package/templates/default/AGENTS.md +3 -1
- package/packages/ui/dist/assets/cursor-DCRoxgSY.js +0 -3
|
@@ -35208,65 +35208,74 @@ var {
|
|
|
35208
35208
|
|
|
35209
35209
|
// ../chains/dist/index.js
|
|
35210
35210
|
var chains = {
|
|
35211
|
-
//
|
|
35212
|
-
|
|
35213
|
-
chainId:
|
|
35214
|
-
name: "
|
|
35215
|
-
//
|
|
35216
|
-
|
|
35217
|
-
|
|
35218
|
-
|
|
35219
|
-
kernel: "0xf1D0F4C9893612627409948BAa9d82a01a373799",
|
|
35220
|
-
mandateFactory: "0xdfF6a2272F667cDf78Af4681b9c88A219998db95",
|
|
35221
|
-
governance: "0xEaD44bC6999E7b00b9b2E11c1660248DC2a30993",
|
|
35211
|
+
// Ethereum mainnet
|
|
35212
|
+
1: {
|
|
35213
|
+
chainId: 1,
|
|
35214
|
+
name: "Ethereum",
|
|
35215
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35216
|
+
kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
|
|
35217
|
+
mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
|
|
35218
|
+
governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
|
|
35222
35219
|
dispatchModel: "selective",
|
|
35223
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35224
35220
|
protocols: {}
|
|
35225
35221
|
},
|
|
35226
35222
|
// Base mainnet
|
|
35227
35223
|
8453: {
|
|
35228
35224
|
chainId: 8453,
|
|
35229
35225
|
name: "Base",
|
|
35230
|
-
//
|
|
35231
|
-
//
|
|
35232
|
-
|
|
35233
|
-
|
|
35234
|
-
|
|
35235
|
-
mandateFactory: "0x7724EACd97C8601d5AC244Aadbf76ad87353Ff31",
|
|
35236
|
-
governance: "0x7E897D919872b1587577617ffFC42113679d0C50",
|
|
35226
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35227
|
+
// Supersedes 0x6319d3dfDDe3804ba93D65752b00c52bFb05a1ab (SAIL-405).
|
|
35228
|
+
kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
|
|
35229
|
+
mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
|
|
35230
|
+
governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
|
|
35237
35231
|
dispatchModel: "selective",
|
|
35238
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35239
35232
|
protocols: {}
|
|
35240
35233
|
},
|
|
35241
35234
|
// Arbitrum mainnet
|
|
35242
35235
|
42161: {
|
|
35243
35236
|
chainId: 42161,
|
|
35244
35237
|
name: "Arbitrum",
|
|
35245
|
-
//
|
|
35246
|
-
//
|
|
35247
|
-
|
|
35248
|
-
|
|
35249
|
-
|
|
35250
|
-
kernel: "0x2716B12832DED0EF5688519c5Fe069EFc0374E02",
|
|
35251
|
-
mandateFactory: "0x23681A8A4C9819D8EaB37E46B858da6F3c85E683",
|
|
35252
|
-
governance: "0xd6AbB7A1036ADc7958Abffec9Da03450c5a2Ec8e",
|
|
35238
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35239
|
+
// Supersedes 0x2716B12832DED0EF5688519c5Fe069EFc0374E02 (SAIL-405).
|
|
35240
|
+
kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
|
|
35241
|
+
mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
|
|
35242
|
+
governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
|
|
35253
35243
|
dispatchModel: "selective",
|
|
35254
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35255
35244
|
protocols: {}
|
|
35256
35245
|
},
|
|
35257
35246
|
// Unichain mainnet
|
|
35258
35247
|
130: {
|
|
35259
35248
|
chainId: 130,
|
|
35260
35249
|
name: "Unichain",
|
|
35261
|
-
//
|
|
35262
|
-
//
|
|
35263
|
-
|
|
35264
|
-
|
|
35265
|
-
|
|
35266
|
-
|
|
35267
|
-
|
|
35250
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35251
|
+
// Supersedes 0xD985029960a9B7C2E7E38e102C448b8b8539B156 (SAIL-406).
|
|
35252
|
+
kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
|
|
35253
|
+
mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
|
|
35254
|
+
governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
|
|
35255
|
+
dispatchModel: "selective",
|
|
35256
|
+
protocols: {}
|
|
35257
|
+
},
|
|
35258
|
+
// Base Sepolia (testnet)
|
|
35259
|
+
84532: {
|
|
35260
|
+
chainId: 84532,
|
|
35261
|
+
name: "Base Sepolia",
|
|
35262
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35263
|
+
// Supersedes 0xf1D0F4C9893612627409948BAa9d82a01a373799 (SAIL-405).
|
|
35264
|
+
kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
|
|
35265
|
+
mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
|
|
35266
|
+
governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
|
|
35267
|
+
dispatchModel: "selective",
|
|
35268
|
+
protocols: {}
|
|
35269
|
+
},
|
|
35270
|
+
// Eth Sepolia (testnet)
|
|
35271
|
+
11155111: {
|
|
35272
|
+
chainId: 11155111,
|
|
35273
|
+
name: "Eth Sepolia",
|
|
35274
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35275
|
+
kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
|
|
35276
|
+
mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
|
|
35277
|
+
governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
|
|
35268
35278
|
dispatchModel: "selective",
|
|
35269
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35270
35279
|
protocols: {}
|
|
35271
35280
|
}
|
|
35272
35281
|
};
|
|
@@ -35690,178 +35699,148 @@ async function detectKernelCapabilities(publicClient, kernel, opts) {
|
|
|
35690
35699
|
}
|
|
35691
35700
|
|
|
35692
35701
|
// ../sdk/dist/deployments.js
|
|
35702
|
+
var CREATE2_KERNEL = "0x02ABC18B65A328de2e749F56ba79ACF2718a6659";
|
|
35703
|
+
var CREATE2_GOVERNANCE = "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC";
|
|
35704
|
+
var CREATE2_TIMELOCK = "0xE48Ba8DB6d748adafD13155c3590f62e58a77f56";
|
|
35705
|
+
var CREATE2_SAFE_MODULE_ENABLER = "0x7897Cb53a4be4a2eaAf46D60573C4Fd83b33fE1F";
|
|
35706
|
+
var CREATE2_MANDATE_FACTORY = "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2";
|
|
35707
|
+
var CREATE2_STANDARD_FEE_POLICY = "0xe7B5901b839cFFDEd9D4108A22712C8BfdA1D80D";
|
|
35708
|
+
var CREATE2_TREASURY = "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6";
|
|
35693
35709
|
var sailDeployments = {
|
|
35694
|
-
|
|
35695
|
-
|
|
35696
|
-
//
|
|
35697
|
-
//
|
|
35698
|
-
|
|
35699
|
-
|
|
35700
|
-
|
|
35701
|
-
|
|
35702
|
-
|
|
35703
|
-
|
|
35704
|
-
|
|
35705
|
-
|
|
35706
|
-
|
|
35707
|
-
|
|
35708
|
-
kernel: "0xf1D0F4C9893612627409948BAa9d82a01a373799",
|
|
35709
|
-
permissionFactory: "0xdfF6a2272F667cDf78Af4681b9c88A219998db95",
|
|
35710
|
-
standardFeePolicy: "0x05570F7973b46Eb9Ed4518422891EFC26BD58b97",
|
|
35711
|
-
safeModuleEnabler: "0xB2C2B52d94412e3472C9fb2B52186eA12a935869",
|
|
35712
|
-
treasury: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
|
|
35710
|
+
// ── Ethereum mainnet ─────────────────────────────────────────────────────────
|
|
35711
|
+
1: {
|
|
35712
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35713
|
+
// allowlistBootstrapped=true (genesis bootstrap), zero fees, 48h timelock.
|
|
35714
|
+
chainId: 1,
|
|
35715
|
+
blockNumber: 25280925,
|
|
35716
|
+
deployer: CREATE2_TREASURY,
|
|
35717
|
+
governance: CREATE2_GOVERNANCE,
|
|
35718
|
+
timelock: CREATE2_TIMELOCK,
|
|
35719
|
+
kernel: CREATE2_KERNEL,
|
|
35720
|
+
mandateFactory: CREATE2_MANDATE_FACTORY,
|
|
35721
|
+
standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
|
|
35722
|
+
safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
|
|
35723
|
+
treasury: CREATE2_TREASURY,
|
|
35713
35724
|
maxPermissionFeeWei: 1000000000000000n,
|
|
35714
35725
|
initialBaseFee: 0n,
|
|
35715
35726
|
initialComplexityRate: 0n,
|
|
35716
35727
|
dispatchModel: "selective",
|
|
35717
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35718
35728
|
knownTemplates: [],
|
|
35719
35729
|
standaloneTemplates: {}
|
|
35720
35730
|
},
|
|
35731
|
+
// ── Base mainnet ─────────────────────────────────────────────────────────────
|
|
35721
35732
|
8453: {
|
|
35722
|
-
//
|
|
35723
|
-
//
|
|
35724
|
-
//
|
|
35725
|
-
// local CREATE2 proxy prediction carried over; allowlistBootstrapped=true,
|
|
35726
|
-
// zero fees, onboarding live. Supersedes 0x20eff0DbE752e22655A6dAA5A94521FA06CDdE06.
|
|
35727
|
-
// Only `core` was redeployed; shared/standalone permission templates are NOT yet
|
|
35728
|
-
// deployed against this kernel (run the templates targets + refill the maps first).
|
|
35733
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
|
|
35734
|
+
// 0x6319d3dfDDe3804ba93D65752b00c52bFb05a1ab (SAIL-405 redeploy).
|
|
35735
|
+
// allowlistBootstrapped=true, zero fees, 48h timelock.
|
|
35729
35736
|
chainId: 8453,
|
|
35730
|
-
blockNumber:
|
|
35731
|
-
deployer:
|
|
35732
|
-
governance:
|
|
35733
|
-
timelock:
|
|
35734
|
-
kernel:
|
|
35735
|
-
|
|
35736
|
-
standardFeePolicy:
|
|
35737
|
-
safeModuleEnabler:
|
|
35738
|
-
treasury:
|
|
35737
|
+
blockNumber: 47115338,
|
|
35738
|
+
deployer: CREATE2_TREASURY,
|
|
35739
|
+
governance: CREATE2_GOVERNANCE,
|
|
35740
|
+
timelock: CREATE2_TIMELOCK,
|
|
35741
|
+
kernel: CREATE2_KERNEL,
|
|
35742
|
+
mandateFactory: CREATE2_MANDATE_FACTORY,
|
|
35743
|
+
standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
|
|
35744
|
+
safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
|
|
35745
|
+
treasury: CREATE2_TREASURY,
|
|
35739
35746
|
maxPermissionFeeWei: 1000000000000000n,
|
|
35740
35747
|
initialBaseFee: 0n,
|
|
35741
35748
|
initialComplexityRate: 0n,
|
|
35742
35749
|
dispatchModel: "selective",
|
|
35743
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35744
35750
|
knownTemplates: [],
|
|
35745
35751
|
standaloneTemplates: {}
|
|
35746
35752
|
},
|
|
35753
|
+
// ── Arbitrum mainnet ─────────────────────────────────────────────────────────
|
|
35747
35754
|
42161: {
|
|
35748
|
-
//
|
|
35749
|
-
//
|
|
35750
|
-
//
|
|
35751
|
-
// local CREATE2 proxy prediction carried over; allowlistBootstrapped=true,
|
|
35752
|
-
// zero fees, onboarding live. Supersedes 0x9AF32E0C395fb31f5cA28994351F8fAE3003e125.
|
|
35753
|
-
// Bootstrap was sent as a standalone tx post-core-deploy; identical end state to Base.
|
|
35754
|
-
// Only `core` was redeployed; shared/standalone permission templates are NOT yet
|
|
35755
|
-
// deployed against this kernel (run the templates targets + refill the maps first).
|
|
35755
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
|
|
35756
|
+
// 0x2716B12832DED0EF5688519c5Fe069EFc0374E02 (SAIL-405 redeploy).
|
|
35757
|
+
// allowlistBootstrapped=true, zero fees, 48h timelock.
|
|
35756
35758
|
chainId: 42161,
|
|
35757
|
-
blockNumber:
|
|
35758
|
-
deployer:
|
|
35759
|
-
governance:
|
|
35760
|
-
timelock:
|
|
35761
|
-
kernel:
|
|
35762
|
-
|
|
35763
|
-
standardFeePolicy:
|
|
35764
|
-
safeModuleEnabler:
|
|
35765
|
-
treasury:
|
|
35759
|
+
blockNumber: 471736462,
|
|
35760
|
+
deployer: CREATE2_TREASURY,
|
|
35761
|
+
governance: CREATE2_GOVERNANCE,
|
|
35762
|
+
timelock: CREATE2_TIMELOCK,
|
|
35763
|
+
kernel: CREATE2_KERNEL,
|
|
35764
|
+
mandateFactory: CREATE2_MANDATE_FACTORY,
|
|
35765
|
+
standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
|
|
35766
|
+
safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
|
|
35767
|
+
treasury: CREATE2_TREASURY,
|
|
35766
35768
|
maxPermissionFeeWei: 1000000000000000n,
|
|
35767
35769
|
initialBaseFee: 0n,
|
|
35768
35770
|
initialComplexityRate: 0n,
|
|
35769
35771
|
dispatchModel: "selective",
|
|
35770
|
-
// selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
|
|
35771
35772
|
knownTemplates: [],
|
|
35772
35773
|
standaloneTemplates: {}
|
|
35773
35774
|
},
|
|
35775
|
+
// ── Unichain mainnet ─────────────────────────────────────────────────────────
|
|
35774
35776
|
130: {
|
|
35775
|
-
//
|
|
35776
|
-
//
|
|
35777
|
-
//
|
|
35778
|
-
//
|
|
35779
|
-
//
|
|
35780
|
-
// 0xd7d408eb…fb4c), zero fees, onboarding live without the 48h timelock.
|
|
35781
|
-
// First chain to ship permission templates against the kernel.
|
|
35777
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
|
|
35778
|
+
// 0xD985029960a9B7C2E7E38e102C448b8b8539B156 (SAIL-406 deploy).
|
|
35779
|
+
// NOTE: knownTemplates and standaloneTemplates from SAIL-406 were deployed
|
|
35780
|
+
// against the old kernel 0xD985029... and are now invalid. They must be
|
|
35781
|
+
// redeployed against the new kernel 0x02ABC1... and re-populated here.
|
|
35782
35782
|
chainId: 130,
|
|
35783
|
-
blockNumber:
|
|
35784
|
-
deployer:
|
|
35785
|
-
governance:
|
|
35786
|
-
timelock:
|
|
35787
|
-
kernel:
|
|
35788
|
-
|
|
35789
|
-
standardFeePolicy:
|
|
35790
|
-
safeModuleEnabler:
|
|
35791
|
-
treasury:
|
|
35783
|
+
blockNumber: 50271704,
|
|
35784
|
+
deployer: CREATE2_TREASURY,
|
|
35785
|
+
governance: CREATE2_GOVERNANCE,
|
|
35786
|
+
timelock: CREATE2_TIMELOCK,
|
|
35787
|
+
kernel: CREATE2_KERNEL,
|
|
35788
|
+
mandateFactory: CREATE2_MANDATE_FACTORY,
|
|
35789
|
+
standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
|
|
35790
|
+
safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
|
|
35791
|
+
treasury: CREATE2_TREASURY,
|
|
35792
35792
|
maxPermissionFeeWei: 1000000000000000n,
|
|
35793
35793
|
initialBaseFee: 0n,
|
|
35794
35794
|
initialComplexityRate: 0n,
|
|
35795
35795
|
dispatchModel: "selective",
|
|
35796
|
-
//
|
|
35797
|
-
|
|
35798
|
-
|
|
35799
|
-
|
|
35800
|
-
|
|
35801
|
-
|
|
35802
|
-
|
|
35803
|
-
|
|
35804
|
-
|
|
35805
|
-
|
|
35806
|
-
|
|
35807
|
-
|
|
35808
|
-
|
|
35809
|
-
|
|
35810
|
-
|
|
35811
|
-
|
|
35812
|
-
|
|
35813
|
-
|
|
35814
|
-
|
|
35815
|
-
|
|
35816
|
-
|
|
35817
|
-
|
|
35818
|
-
|
|
35819
|
-
|
|
35820
|
-
|
|
35821
|
-
|
|
35822
|
-
|
|
35823
|
-
|
|
35824
|
-
|
|
35825
|
-
|
|
35826
|
-
|
|
35827
|
-
|
|
35828
|
-
|
|
35829
|
-
|
|
35830
|
-
|
|
35831
|
-
|
|
35832
|
-
|
|
35833
|
-
|
|
35834
|
-
|
|
35835
|
-
|
|
35836
|
-
|
|
35837
|
-
|
|
35838
|
-
|
|
35839
|
-
|
|
35840
|
-
|
|
35841
|
-
|
|
35842
|
-
|
|
35843
|
-
|
|
35844
|
-
label: "Shared Transfer Target",
|
|
35845
|
-
description: "Allows transfers only to a pre-approved target address."
|
|
35846
|
-
}
|
|
35847
|
-
],
|
|
35848
|
-
standaloneTemplates: {
|
|
35849
|
-
// EIP-1167 clone LOGIC addresses — the `impl` argument to
|
|
35850
|
-
// PermissionFactory.deployAndAttach(account, impl, salt, initData). A clone
|
|
35851
|
-
// is created and configured per account via its initialize(...).
|
|
35852
|
-
azuroPrediction: "0xd48cdBB25bF0A214dEffECac3c9431650834b046",
|
|
35853
|
-
boundedApprove: "0xbF7089A905081054c9dA628707f2e1EF70A7F300",
|
|
35854
|
-
boundedBorrow: "0x17D466309C7E0237960f68126Cc4A109D194ac28",
|
|
35855
|
-
boundedDeposit: "0xf49E304EDf806AF46E8f17740e56C1CBFad5d264",
|
|
35856
|
-
boundedLiFi: "0x6a0171013FeD6B2Eda16A4dd4DB33Fa34b7F3e3f",
|
|
35857
|
-
boundedSwap: "0x06696F9dd4bD0994f55b075600627Dc6E54635c9",
|
|
35858
|
-
boundedWithdraw: "0xE207CfC8c2204b15ee5fD22B79472929706c7E4b",
|
|
35859
|
-
gmxPerp: "0xB1bb967aC11D61C0599c8458D9B950461db5D4E9",
|
|
35860
|
-
gainsNetworkPerp: "0x1297673f71A9be02bc876Dbd0ceaB3c96D268bE3",
|
|
35861
|
-
limitlessPrediction: "0x2bE4280d8816626e1dea4E94A83d9334A971AF90",
|
|
35862
|
-
synthetixPerp: "0x711a70B16D013a9B96Bd6733F4b3097e5787f860",
|
|
35863
|
-
transferTarget: "0x8428155b6b9eea4E78b9a52c2312752eD04Baf16"
|
|
35864
|
-
}
|
|
35796
|
+
// Templates cleared: the SAIL-406 shared + standalone templates were deployed
|
|
35797
|
+
// against the old kernel (0xD985029...) and are invalid against the new one.
|
|
35798
|
+
// Re-populate after redeploying templates against 0x02ABC1...
|
|
35799
|
+
knownTemplates: [],
|
|
35800
|
+
standaloneTemplates: {}
|
|
35801
|
+
},
|
|
35802
|
+
// ── Base Sepolia (testnet) ───────────────────────────────────────────────────
|
|
35803
|
+
84532: {
|
|
35804
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
|
|
35805
|
+
// 0xf1D0F4C9893612627409948BAa9d82a01a373799 (SAIL-405 redeploy).
|
|
35806
|
+
// allowlistBootstrapped=true, zero fees, 48h timelock.
|
|
35807
|
+
chainId: 84532,
|
|
35808
|
+
blockNumber: 42625843,
|
|
35809
|
+
deployer: CREATE2_TREASURY,
|
|
35810
|
+
governance: CREATE2_GOVERNANCE,
|
|
35811
|
+
timelock: CREATE2_TIMELOCK,
|
|
35812
|
+
kernel: CREATE2_KERNEL,
|
|
35813
|
+
mandateFactory: CREATE2_MANDATE_FACTORY,
|
|
35814
|
+
standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
|
|
35815
|
+
safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
|
|
35816
|
+
treasury: CREATE2_TREASURY,
|
|
35817
|
+
maxPermissionFeeWei: 1000000000000000n,
|
|
35818
|
+
initialBaseFee: 0n,
|
|
35819
|
+
initialComplexityRate: 0n,
|
|
35820
|
+
dispatchModel: "selective",
|
|
35821
|
+
knownTemplates: [],
|
|
35822
|
+
standaloneTemplates: {}
|
|
35823
|
+
},
|
|
35824
|
+
// ── Eth Sepolia (testnet) ────────────────────────────────────────────────────
|
|
35825
|
+
11155111: {
|
|
35826
|
+
// CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
|
|
35827
|
+
// allowlistBootstrapped=true, zero fees, 48h timelock.
|
|
35828
|
+
chainId: 11155111,
|
|
35829
|
+
blockNumber: 11023571,
|
|
35830
|
+
deployer: CREATE2_TREASURY,
|
|
35831
|
+
governance: CREATE2_GOVERNANCE,
|
|
35832
|
+
timelock: CREATE2_TIMELOCK,
|
|
35833
|
+
kernel: CREATE2_KERNEL,
|
|
35834
|
+
mandateFactory: CREATE2_MANDATE_FACTORY,
|
|
35835
|
+
standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
|
|
35836
|
+
safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
|
|
35837
|
+
treasury: CREATE2_TREASURY,
|
|
35838
|
+
maxPermissionFeeWei: 1000000000000000n,
|
|
35839
|
+
initialBaseFee: 0n,
|
|
35840
|
+
initialComplexityRate: 0n,
|
|
35841
|
+
dispatchModel: "selective",
|
|
35842
|
+
knownTemplates: [],
|
|
35843
|
+
standaloneTemplates: {}
|
|
35865
35844
|
}
|
|
35866
35845
|
};
|
|
35867
35846
|
function getSailDeployment(chainId) {
|
|
@@ -38141,6 +38120,68 @@ var baseSepoliaPreconf = /* @__PURE__ */ defineChain({
|
|
|
38141
38120
|
}
|
|
38142
38121
|
});
|
|
38143
38122
|
|
|
38123
|
+
// ../../node_modules/.pnpm/viem@2.51.3_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@5.0.10_zod@4.4.3/node_modules/viem/_esm/chains/definitions/mainnet.js
|
|
38124
|
+
init_defineChain();
|
|
38125
|
+
var mainnet = /* @__PURE__ */ defineChain({
|
|
38126
|
+
id: 1,
|
|
38127
|
+
name: "Ethereum",
|
|
38128
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
38129
|
+
blockTime: 12e3,
|
|
38130
|
+
rpcUrls: {
|
|
38131
|
+
default: {
|
|
38132
|
+
http: ["https://eth.merkle.io"]
|
|
38133
|
+
}
|
|
38134
|
+
},
|
|
38135
|
+
blockExplorers: {
|
|
38136
|
+
default: {
|
|
38137
|
+
name: "Etherscan",
|
|
38138
|
+
url: "https://etherscan.io",
|
|
38139
|
+
apiUrl: "https://api.etherscan.io/api"
|
|
38140
|
+
}
|
|
38141
|
+
},
|
|
38142
|
+
contracts: {
|
|
38143
|
+
ensUniversalResolver: {
|
|
38144
|
+
address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee",
|
|
38145
|
+
blockCreated: 23085558
|
|
38146
|
+
},
|
|
38147
|
+
multicall3: {
|
|
38148
|
+
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
|
38149
|
+
blockCreated: 14353601
|
|
38150
|
+
}
|
|
38151
|
+
}
|
|
38152
|
+
});
|
|
38153
|
+
|
|
38154
|
+
// ../../node_modules/.pnpm/viem@2.51.3_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@5.0.10_zod@4.4.3/node_modules/viem/_esm/chains/definitions/sepolia.js
|
|
38155
|
+
init_defineChain();
|
|
38156
|
+
var sepolia = /* @__PURE__ */ defineChain({
|
|
38157
|
+
id: 11155111,
|
|
38158
|
+
name: "Sepolia",
|
|
38159
|
+
nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 },
|
|
38160
|
+
rpcUrls: {
|
|
38161
|
+
default: {
|
|
38162
|
+
http: ["https://11155111.rpc.thirdweb.com"]
|
|
38163
|
+
}
|
|
38164
|
+
},
|
|
38165
|
+
blockExplorers: {
|
|
38166
|
+
default: {
|
|
38167
|
+
name: "Etherscan",
|
|
38168
|
+
url: "https://sepolia.etherscan.io",
|
|
38169
|
+
apiUrl: "https://api-sepolia.etherscan.io/api"
|
|
38170
|
+
}
|
|
38171
|
+
},
|
|
38172
|
+
contracts: {
|
|
38173
|
+
multicall3: {
|
|
38174
|
+
address: "0xca11bde05977b3631167028862be2a173976ca11",
|
|
38175
|
+
blockCreated: 751532
|
|
38176
|
+
},
|
|
38177
|
+
ensUniversalResolver: {
|
|
38178
|
+
address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee",
|
|
38179
|
+
blockCreated: 8928790
|
|
38180
|
+
}
|
|
38181
|
+
},
|
|
38182
|
+
testnet: true
|
|
38183
|
+
});
|
|
38184
|
+
|
|
38144
38185
|
// ../../node_modules/.pnpm/viem@2.51.3_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@5.0.10_zod@4.4.3/node_modules/viem/_esm/chains/definitions/unichain.js
|
|
38145
38186
|
init_defineChain();
|
|
38146
38187
|
var sourceId3 = 1;
|
|
@@ -38339,25 +38380,29 @@ async function promptHidden(question) {
|
|
|
38339
38380
|
|
|
38340
38381
|
// src/lib/chain.ts
|
|
38341
38382
|
var CHAINS = {
|
|
38383
|
+
1: mainnet,
|
|
38342
38384
|
8453: base,
|
|
38343
|
-
84532: baseSepolia,
|
|
38344
38385
|
42161: arbitrum,
|
|
38345
|
-
130: unichain
|
|
38386
|
+
130: unichain,
|
|
38387
|
+
84532: baseSepolia,
|
|
38388
|
+
11155111: sepolia
|
|
38346
38389
|
};
|
|
38347
38390
|
function getChainById(chainId) {
|
|
38348
38391
|
const chain2 = CHAINS[chainId];
|
|
38349
38392
|
if (!chain2) {
|
|
38350
38393
|
throw new Error(
|
|
38351
|
-
`Unsupported chainId: ${chainId}. Supported:
|
|
38394
|
+
`Unsupported chainId: ${chainId}. Supported: 1 (Ethereum), 8453 (Base), 42161 (Arbitrum), 130 (Unichain), 84532 (Base Sepolia), 11155111 (Eth Sepolia)`
|
|
38352
38395
|
);
|
|
38353
38396
|
}
|
|
38354
38397
|
return chain2;
|
|
38355
38398
|
}
|
|
38356
38399
|
var RPC_ENV_VARS = {
|
|
38400
|
+
1: "ETH_MAINNET_RPC_URL",
|
|
38357
38401
|
8453: "BASE_RPC_URL",
|
|
38358
|
-
84532: "BASE_SEPOLIA_RPC_URL",
|
|
38359
38402
|
42161: "ARBITRUM_RPC_URL",
|
|
38360
|
-
130: "UNICHAIN_RPC_URL"
|
|
38403
|
+
130: "UNICHAIN_RPC_URL",
|
|
38404
|
+
84532: "BASE_SEPOLIA_RPC_URL",
|
|
38405
|
+
11155111: "SEPOLIA_RPC_URL"
|
|
38361
38406
|
};
|
|
38362
38407
|
function getRpcUrl(chainId) {
|
|
38363
38408
|
const env = parseEnvFile(sailPath(".env.local"));
|
|
@@ -38443,2054 +38488,2312 @@ async function loadAnySigner() {
|
|
|
38443
38488
|
throw new Error('No signing key found.\nRun "sailor keys generate" first.');
|
|
38444
38489
|
}
|
|
38445
38490
|
|
|
38446
|
-
// src/lib/
|
|
38491
|
+
// src/lib/packagePaths.ts
|
|
38447
38492
|
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
38448
38493
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
38494
|
+
function cliDistDir() {
|
|
38495
|
+
try {
|
|
38496
|
+
return import_node_path2.default.dirname(import_node_fs3.default.realpathSync(process.argv[1]));
|
|
38497
|
+
} catch {
|
|
38498
|
+
return import_node_path2.default.dirname(import_node_path2.default.resolve(process.argv[1]));
|
|
38499
|
+
}
|
|
38500
|
+
}
|
|
38501
|
+
function packageRoot() {
|
|
38502
|
+
let dir = cliDistDir();
|
|
38503
|
+
let firstBinMatch = null;
|
|
38504
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
38505
|
+
const pkgFile = import_node_path2.default.join(dir, "package.json");
|
|
38506
|
+
if (import_node_fs3.default.existsSync(pkgFile)) {
|
|
38507
|
+
try {
|
|
38508
|
+
const pkg = JSON.parse(import_node_fs3.default.readFileSync(pkgFile, "utf-8"));
|
|
38509
|
+
if (pkg.bin?.sailor) {
|
|
38510
|
+
if (firstBinMatch === null) firstBinMatch = dir;
|
|
38511
|
+
if (import_node_fs3.default.existsSync(import_node_path2.default.join(dir, "templates"))) return dir;
|
|
38512
|
+
}
|
|
38513
|
+
} catch {
|
|
38514
|
+
}
|
|
38515
|
+
}
|
|
38516
|
+
const parent = import_node_path2.default.dirname(dir);
|
|
38517
|
+
if (parent === dir) break;
|
|
38518
|
+
dir = parent;
|
|
38519
|
+
}
|
|
38520
|
+
return firstBinMatch ?? import_node_path2.default.resolve(cliDistDir(), "../../..");
|
|
38521
|
+
}
|
|
38522
|
+
function projectPort(projectRoot) {
|
|
38523
|
+
const hash3 = [...projectRoot].reduce((h, c) => (h << 5) - h + c.charCodeAt(0) >>> 0, 0);
|
|
38524
|
+
return 3333 + hash3 % 667;
|
|
38525
|
+
}
|
|
38526
|
+
|
|
38527
|
+
// src/lib/state.ts
|
|
38528
|
+
var import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
38529
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
38449
38530
|
function upsertAccountInList(account2, name, baseSailDir = sailDir()) {
|
|
38450
|
-
const accountsPath =
|
|
38531
|
+
const accountsPath = import_node_path3.default.join(baseSailDir, "state", "accounts.json");
|
|
38451
38532
|
let accounts = [];
|
|
38452
38533
|
try {
|
|
38453
|
-
accounts = JSON.parse(
|
|
38534
|
+
accounts = JSON.parse(import_node_fs4.default.readFileSync(accountsPath, "utf-8"));
|
|
38454
38535
|
} catch {
|
|
38455
38536
|
try {
|
|
38456
38537
|
const prev = JSON.parse(
|
|
38457
|
-
|
|
38538
|
+
import_node_fs4.default.readFileSync(import_node_path3.default.join(baseSailDir, "account.json"), "utf-8")
|
|
38458
38539
|
);
|
|
38459
38540
|
if (prev?.safe) accounts.push({ ...prev, name: "SMA 1", addedAt: null });
|
|
38460
38541
|
} catch {
|
|
38461
38542
|
}
|
|
38462
38543
|
}
|
|
38463
|
-
|
|
38544
|
+
const idx = accounts.findIndex((a) => a.safe.toLowerCase() === account2.safe.toLowerCase());
|
|
38545
|
+
if (idx === -1) {
|
|
38464
38546
|
accounts.push({
|
|
38465
38547
|
...account2,
|
|
38466
38548
|
name: name ?? `SMA ${accounts.length + 1}`,
|
|
38467
38549
|
addedAt: nowIso()
|
|
38468
38550
|
});
|
|
38551
|
+
} else {
|
|
38552
|
+
accounts[idx] = { ...accounts[idx], ...account2 };
|
|
38469
38553
|
}
|
|
38470
|
-
|
|
38471
|
-
|
|
38554
|
+
import_node_fs4.default.mkdirSync(import_node_path3.default.join(baseSailDir, "state"), { recursive: true });
|
|
38555
|
+
import_node_fs4.default.writeFileSync(accountsPath, `${JSON.stringify(accounts, null, 2)}
|
|
38472
38556
|
`);
|
|
38473
38557
|
}
|
|
38474
38558
|
|
|
38475
|
-
// src/
|
|
38476
|
-
|
|
38477
|
-
|
|
38478
|
-
|
|
38479
|
-
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
38483
|
-
|
|
38484
|
-
|
|
38485
|
-
|
|
38559
|
+
// src/signing/client.ts
|
|
38560
|
+
var import_node_fs6 = require("node:fs");
|
|
38561
|
+
var import_node_path5 = require("node:path");
|
|
38562
|
+
|
|
38563
|
+
// src/signing/server.ts
|
|
38564
|
+
var import_node_crypto2 = require("node:crypto");
|
|
38565
|
+
var import_node_fs5 = require("node:fs");
|
|
38566
|
+
var import_node_http = require("node:http");
|
|
38567
|
+
var import_node_net = require("node:net");
|
|
38568
|
+
var import_node_path4 = require("node:path");
|
|
38569
|
+
|
|
38570
|
+
// ../../node_modules/.pnpm/ws@8.21.0_bufferutil@4.1.0_utf-8-validate@5.0.10/node_modules/ws/wrapper.mjs
|
|
38571
|
+
var import_stream2 = __toESM(require_stream2(), 1);
|
|
38572
|
+
var import_extension2 = __toESM(require_extension2(), 1);
|
|
38573
|
+
var import_permessage_deflate2 = __toESM(require_permessage_deflate2(), 1);
|
|
38574
|
+
var import_receiver2 = __toESM(require_receiver2(), 1);
|
|
38575
|
+
var import_sender2 = __toESM(require_sender2(), 1);
|
|
38576
|
+
var import_subprotocol2 = __toESM(require_subprotocol2(), 1);
|
|
38577
|
+
var import_websocket2 = __toESM(require_websocket2(), 1);
|
|
38578
|
+
var import_websocket_server2 = __toESM(require_websocket_server2(), 1);
|
|
38579
|
+
|
|
38580
|
+
// src/signing/server.ts
|
|
38581
|
+
var DEFAULT_SIGNING_PORT = 3141;
|
|
38582
|
+
var RUNTIME_SUBDIR = (0, import_node_path4.join)(".sail", "runtime");
|
|
38583
|
+
var SERVER_STATE_FILE = "server.json";
|
|
38584
|
+
var REQUEST_SECRET_HEADER = "x-sailor-secret";
|
|
38585
|
+
var MIME = {
|
|
38586
|
+
".html": "text/html; charset=utf-8",
|
|
38587
|
+
".js": "application/javascript",
|
|
38588
|
+
".mjs": "application/javascript",
|
|
38589
|
+
".css": "text/css",
|
|
38590
|
+
".svg": "image/svg+xml",
|
|
38591
|
+
".png": "image/png",
|
|
38592
|
+
".ico": "image/x-icon",
|
|
38593
|
+
".json": "application/json",
|
|
38594
|
+
".woff": "font/woff",
|
|
38595
|
+
".woff2": "font/woff2"
|
|
38596
|
+
};
|
|
38597
|
+
function findUiDist() {
|
|
38598
|
+
const candidates = [
|
|
38599
|
+
// Installed package (any scope): walk up to package root via bin.sailor marker
|
|
38600
|
+
(0, import_node_path4.join)(packageRoot(), "packages", "ui", "dist"),
|
|
38601
|
+
// Monorepo dev via tsx run from the repo root
|
|
38602
|
+
(0, import_node_path4.join)(process.cwd(), "packages", "ui", "dist"),
|
|
38603
|
+
(0, import_node_path4.join)(process.cwd(), "..", "ui", "dist")
|
|
38604
|
+
];
|
|
38605
|
+
for (const c of candidates) {
|
|
38606
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(c, "index.html"))) return c;
|
|
38486
38607
|
}
|
|
38608
|
+
return null;
|
|
38487
38609
|
}
|
|
38488
|
-
|
|
38489
|
-
|
|
38490
|
-
|
|
38491
|
-
|
|
38492
|
-
|
|
38610
|
+
var RESULT_LONGPOLL_MS = 25e3;
|
|
38611
|
+
var SigningServer = class {
|
|
38612
|
+
/** This channel owns the server in-process (see SigningChannel). */
|
|
38613
|
+
remote = false;
|
|
38614
|
+
projectRoot;
|
|
38615
|
+
runtimeDir;
|
|
38616
|
+
port;
|
|
38617
|
+
_url = "";
|
|
38618
|
+
pending = /* @__PURE__ */ new Map();
|
|
38619
|
+
results = /* @__PURE__ */ new Map();
|
|
38620
|
+
resultWaiters = /* @__PURE__ */ new Map();
|
|
38621
|
+
clients = /* @__PURE__ */ new Set();
|
|
38622
|
+
httpServer = null;
|
|
38623
|
+
wss = null;
|
|
38624
|
+
_connectedWallet;
|
|
38625
|
+
walletListeners = [];
|
|
38626
|
+
uiDist;
|
|
38627
|
+
/**
|
|
38628
|
+
* Whether to publish .sail/runtime/server.json (the daemon-discovery hint).
|
|
38629
|
+
* The persistent daemon (`sailor station start`) advertises; ephemeral
|
|
38630
|
+
* per-command servers do not, so they never clobber a running daemon's state
|
|
38631
|
+
* on a discovery race. The browser UI finds servers by port-probing anyway.
|
|
38632
|
+
*/
|
|
38633
|
+
advertise;
|
|
38634
|
+
/** Random secret generated at startup. Required on POST /requests to prevent
|
|
38635
|
+
* cross-origin pages from injecting signing requests. */
|
|
38636
|
+
requestSecret = "";
|
|
38637
|
+
constructor(opts = {}) {
|
|
38638
|
+
this.projectRoot = opts.projectRoot ?? process.cwd();
|
|
38639
|
+
this.runtimeDir = (0, import_node_path4.join)(this.projectRoot, RUNTIME_SUBDIR);
|
|
38640
|
+
this.port = opts.port ?? DEFAULT_SIGNING_PORT;
|
|
38641
|
+
this.uiDist = opts.uiDist ?? findUiDist();
|
|
38642
|
+
this.advertise = opts.advertise ?? true;
|
|
38493
38643
|
}
|
|
38494
|
-
|
|
38495
|
-
|
|
38496
|
-
const chainIdRaw = env["CHAIN_ID"] ?? process.env["CHAIN_ID"];
|
|
38497
|
-
if (!rpcUrl || !chainIdRaw) {
|
|
38498
|
-
throw new Error(
|
|
38499
|
-
"RPC_URL and CHAIN_ID must be set in .sail/.env.local.\nCreate that file with, for example:\n RPC_URL=https://your-rpc-endpoint\n CHAIN_ID=8453"
|
|
38500
|
-
);
|
|
38644
|
+
get url() {
|
|
38645
|
+
return this._url;
|
|
38501
38646
|
}
|
|
38502
|
-
|
|
38503
|
-
|
|
38504
|
-
throw new Error(`Invalid CHAIN_ID: "${chainIdRaw}" \u2014 must be a number.`);
|
|
38647
|
+
get wsUrl() {
|
|
38648
|
+
return this._url.replace("http://", "ws://");
|
|
38505
38649
|
}
|
|
38506
|
-
|
|
38507
|
-
|
|
38508
|
-
console.log(` SailKernel: ${checksum4(chain2.kernel)}`);
|
|
38509
|
-
console.log(` Mandate factory: ${checksum4(chain2.mandateFactory)}
|
|
38510
|
-
`);
|
|
38511
|
-
const manager = await loadKeyring("manager");
|
|
38512
|
-
const managerAddr = checksum4(manager.address);
|
|
38513
|
-
const safeFactory = await promptAddress("Safe factory address");
|
|
38514
|
-
const safeSingleton = await promptAddress("Safe singleton address");
|
|
38515
|
-
const owner2 = await promptAddress("Owner (EOA) address", managerAddr);
|
|
38516
|
-
const permissionSigner = await promptAddress("Mandate signer address", managerAddr);
|
|
38517
|
-
const feePolicy = await prompt("Fee policy", "none");
|
|
38518
|
-
console.log("\nCreating SMA with:");
|
|
38519
|
-
console.log(` Owner: ${owner2}`);
|
|
38520
|
-
console.log(` Agent wallet: ${managerAddr}`);
|
|
38521
|
-
console.log(` Mandate signer: ${permissionSigner}`);
|
|
38522
|
-
console.log(` Safe factory: ${safeFactory}`);
|
|
38523
|
-
console.log(` Safe singleton: ${safeSingleton}`);
|
|
38524
|
-
console.log(` Fee policy: ${feePolicy}`);
|
|
38525
|
-
const client = makeClient(chainId);
|
|
38526
|
-
try {
|
|
38527
|
-
const account2 = await client.account.create({
|
|
38528
|
-
owner: owner2,
|
|
38529
|
-
permissionSigner,
|
|
38530
|
-
manager: managerAddr,
|
|
38531
|
-
chainId
|
|
38532
|
-
});
|
|
38533
|
-
const stored = {
|
|
38534
|
-
safe: checksum4(account2.safe),
|
|
38535
|
-
owner: checksum4(account2.owner),
|
|
38536
|
-
permissionSigner: checksum4(account2.permissionSigner),
|
|
38537
|
-
manager: checksum4(account2.manager),
|
|
38538
|
-
chainId: account2.chainId,
|
|
38539
|
-
createdAtBlock: account2.createdAtBlock.toString()
|
|
38540
|
-
};
|
|
38541
|
-
upsertAccountInList(stored);
|
|
38542
|
-
writeJsonFile(sailPath("account.json"), stored);
|
|
38543
|
-
console.log(`
|
|
38544
|
-
SMA created. Address: ${stored.safe}`);
|
|
38545
|
-
console.log("Saved to .sail/account.json");
|
|
38546
|
-
} catch (err) {
|
|
38547
|
-
if (err.message === "not implemented") {
|
|
38548
|
-
console.log(
|
|
38549
|
-
"\nOn-chain account creation is not wired up in this build yet \u2014\nclient.account.create is a stub until SailKernel is deployed and the\nSDK is connected. Nothing was created on-chain."
|
|
38550
|
-
);
|
|
38551
|
-
return;
|
|
38552
|
-
}
|
|
38553
|
-
throw err;
|
|
38650
|
+
get isRunning() {
|
|
38651
|
+
return this.httpServer?.listening ?? false;
|
|
38554
38652
|
}
|
|
38555
|
-
|
|
38556
|
-
|
|
38557
|
-
|
|
38558
|
-
|
|
38559
|
-
|
|
38560
|
-
|
|
38561
|
-
|
|
38562
|
-
|
|
38563
|
-
|
|
38564
|
-
|
|
38565
|
-
|
|
38566
|
-
|
|
38567
|
-
|
|
38568
|
-
}
|
|
38569
|
-
|
|
38570
|
-
|
|
38571
|
-
|
|
38572
|
-
|
|
38573
|
-
|
|
38574
|
-
|
|
38575
|
-
|
|
38576
|
-
|
|
38577
|
-
}
|
|
38578
|
-
|
|
38579
|
-
|
|
38580
|
-
|
|
38653
|
+
async start() {
|
|
38654
|
+
this.port = await findAvailablePort(this.port);
|
|
38655
|
+
this._url = `http://localhost:${this.port}`;
|
|
38656
|
+
this.requestSecret = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
38657
|
+
const http2 = (0, import_node_http.createServer)((req, res) => this.handleHttp(req, res));
|
|
38658
|
+
this.wss = new import_websocket_server2.default({ server: http2 });
|
|
38659
|
+
this.wss.on("connection", (ws, req) => {
|
|
38660
|
+
const params = new URL(req.url ?? "/", this._url).searchParams;
|
|
38661
|
+
if (params.get("secret") !== this.requestSecret) {
|
|
38662
|
+
ws.close(1008, "Unauthorized");
|
|
38663
|
+
return;
|
|
38664
|
+
}
|
|
38665
|
+
this.handleConnection(ws);
|
|
38666
|
+
});
|
|
38667
|
+
await new Promise((res, rej) => {
|
|
38668
|
+
http2.listen(this.port, "127.0.0.1", res);
|
|
38669
|
+
http2.once("error", rej);
|
|
38670
|
+
});
|
|
38671
|
+
this.httpServer = http2;
|
|
38672
|
+
if (this.advertise) this.writeRuntimeState();
|
|
38673
|
+
process.once("SIGINT", () => this.stop());
|
|
38674
|
+
process.once("SIGTERM", () => this.stop());
|
|
38675
|
+
}
|
|
38676
|
+
stop() {
|
|
38677
|
+
for (const [id, entry] of this.pending) {
|
|
38678
|
+
clearTimeout(entry.timer);
|
|
38679
|
+
this.recordResult(
|
|
38680
|
+
{ status: "rejected", requestId: id, reason: "Signing server stopped" },
|
|
38681
|
+
entry.request
|
|
38581
38682
|
);
|
|
38582
38683
|
}
|
|
38583
|
-
|
|
38584
|
-
|
|
38585
|
-
|
|
38586
|
-
|
|
38587
|
-
|
|
38588
|
-
|
|
38684
|
+
this.pending.clear();
|
|
38685
|
+
for (const ws of this.clients) {
|
|
38686
|
+
try {
|
|
38687
|
+
ws.close();
|
|
38688
|
+
} catch {
|
|
38689
|
+
}
|
|
38589
38690
|
}
|
|
38590
|
-
|
|
38591
|
-
|
|
38592
|
-
|
|
38593
|
-
|
|
38594
|
-
|
|
38595
|
-
"The predicted address depends on the agent (manager) wallet, which is mixed into the kernel's CREATE2 salt.\nPass --manager <agent address> (create one first with `sailor keys`), or run after onboarding so it can be read from .sail/account.json."
|
|
38596
|
-
);
|
|
38691
|
+
this.clients.clear();
|
|
38692
|
+
this.wss?.close();
|
|
38693
|
+
this.httpServer?.close();
|
|
38694
|
+
this.httpServer = null;
|
|
38695
|
+
if (this.advertise) this.removeRuntimeState();
|
|
38597
38696
|
}
|
|
38598
|
-
|
|
38599
|
-
|
|
38600
|
-
if (options.chain) {
|
|
38601
|
-
const chainId = Number(options.chain);
|
|
38602
|
-
if (!(chainId in sailDeployments)) {
|
|
38603
|
-
throw new Error(
|
|
38604
|
-
`Chain ${chainId} has no Sail Protocol deployment. Supported: ${Object.keys(sailDeployments).join(", ")}`
|
|
38605
|
-
);
|
|
38606
|
-
}
|
|
38607
|
-
chainIds = [chainId];
|
|
38608
|
-
} else {
|
|
38609
|
-
chainIds = SAIL_MAINNET_CHAINS;
|
|
38697
|
+
get connectedWallet() {
|
|
38698
|
+
return this._connectedWallet;
|
|
38610
38699
|
}
|
|
38611
|
-
|
|
38612
|
-
|
|
38613
|
-
|
|
38614
|
-
|
|
38615
|
-
|
|
38616
|
-
|
|
38617
|
-
|
|
38618
|
-
|
|
38619
|
-
|
|
38620
|
-
|
|
38621
|
-
|
|
38622
|
-
|
|
38623
|
-
|
|
38624
|
-
|
|
38625
|
-
|
|
38626
|
-
permissionSigner: ownerAddr,
|
|
38627
|
-
manager: managerAddr,
|
|
38628
|
-
feePolicy: deployment.standardFeePolicy,
|
|
38629
|
-
proxyCreationCode
|
|
38700
|
+
/**
|
|
38701
|
+
* Resolves as soon as a wallet connects (or immediately if one already is).
|
|
38702
|
+
* The CLI calls this before building calldata that needs the owner's address.
|
|
38703
|
+
*/
|
|
38704
|
+
waitForWallet(timeoutMs = 5 * 60 * 1e3) {
|
|
38705
|
+
if (this._connectedWallet) return Promise.resolve(this._connectedWallet);
|
|
38706
|
+
return new Promise((res, rej) => {
|
|
38707
|
+
const timer = setTimeout(
|
|
38708
|
+
() => rej(new Error("Timed out waiting for wallet connection in the signing UI")),
|
|
38709
|
+
timeoutMs
|
|
38710
|
+
);
|
|
38711
|
+
this.walletListeners.push((addr) => {
|
|
38712
|
+
clearTimeout(timer);
|
|
38713
|
+
res(addr);
|
|
38714
|
+
});
|
|
38630
38715
|
});
|
|
38631
|
-
return {
|
|
38632
|
-
chainId,
|
|
38633
|
-
name: viemChain.name,
|
|
38634
|
-
predictedAddress,
|
|
38635
|
-
kernel: deployment.kernel,
|
|
38636
|
-
safeModuleEnabler: deployment.safeModuleEnabler
|
|
38637
|
-
};
|
|
38638
|
-
});
|
|
38639
|
-
const uniqueAddresses = new Set(results.map((r) => r.predictedAddress.toLowerCase()));
|
|
38640
|
-
const allSame = uniqueAddresses.size === 1;
|
|
38641
|
-
if (options.json) {
|
|
38642
|
-
console.log(
|
|
38643
|
-
JSON.stringify(
|
|
38644
|
-
{
|
|
38645
|
-
salt: saltNonce.toString(),
|
|
38646
|
-
owner: ownerAddr,
|
|
38647
|
-
manager: managerAddr,
|
|
38648
|
-
chains: results.map(({ chainId, name, predictedAddress }) => ({
|
|
38649
|
-
chainId,
|
|
38650
|
-
name,
|
|
38651
|
-
predictedAddress
|
|
38652
|
-
})),
|
|
38653
|
-
allSame,
|
|
38654
|
-
note: allSame ? "All chains produce the same address with this salt, owner, and manager." : "Addresses differ per chain because the kernel salt binds the chain-specific fee policy and the Safe initializer encodes chain-specific contract addresses (kernel, safeModuleEnabler). Cross-chain same-address requires deterministic protocol deployment or a registerExisting() flow."
|
|
38655
|
-
},
|
|
38656
|
-
null,
|
|
38657
|
-
2
|
|
38658
|
-
)
|
|
38659
|
-
);
|
|
38660
|
-
return;
|
|
38661
|
-
}
|
|
38662
|
-
console.log("\nPredicted Safe addresses");
|
|
38663
|
-
console.log(` Owner: ${ownerAddr}`);
|
|
38664
|
-
console.log(` Manager: ${managerAddr}`);
|
|
38665
|
-
console.log(` Salt: ${saltNonce}`);
|
|
38666
|
-
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
38667
|
-
for (const { chainId, name, predictedAddress } of results) {
|
|
38668
|
-
console.log(` ${name.padEnd(14)} (${String(chainId).padEnd(5)}): ${predictedAddress}`);
|
|
38669
38716
|
}
|
|
38670
|
-
|
|
38671
|
-
|
|
38672
|
-
|
|
38673
|
-
|
|
38674
|
-
|
|
38675
|
-
|
|
38676
|
-
|
|
38677
|
-
);
|
|
38717
|
+
/**
|
|
38718
|
+
* Enqueue a signing request and broadcast it to the UI. Returns the full
|
|
38719
|
+
* request (with generated id). Used by both the in-process path and the HTTP
|
|
38720
|
+
* control plane (POST /requests).
|
|
38721
|
+
*/
|
|
38722
|
+
enqueue(req, timeoutMs = 10 * 60 * 1e3) {
|
|
38723
|
+
const id = `req_${Date.now()}_${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
|
|
38724
|
+
const request = { ...req, id, createdAt: Date.now() };
|
|
38725
|
+
const timer = setTimeout(() => {
|
|
38726
|
+
if (this.pending.has(id)) {
|
|
38727
|
+
this.pending.delete(id);
|
|
38728
|
+
this.recordResult(
|
|
38729
|
+
{
|
|
38730
|
+
status: "rejected",
|
|
38731
|
+
requestId: id,
|
|
38732
|
+
reason: `timed out after ${timeoutMs / 1e3}s`
|
|
38733
|
+
},
|
|
38734
|
+
request
|
|
38735
|
+
);
|
|
38736
|
+
}
|
|
38737
|
+
}, timeoutMs);
|
|
38738
|
+
this.pending.set(id, { request, timer });
|
|
38739
|
+
this.broadcast({ type: "request", request });
|
|
38740
|
+
return request;
|
|
38678
38741
|
}
|
|
38679
|
-
|
|
38680
|
-
|
|
38681
|
-
|
|
38682
|
-
|
|
38683
|
-
|
|
38684
|
-
|
|
38685
|
-
|
|
38686
|
-
|
|
38687
|
-
|
|
38688
|
-
|
|
38742
|
+
/**
|
|
38743
|
+
* Resolve once a result for `id` is available (immediately if already
|
|
38744
|
+
* resolved). Resolves to `null` if `timeoutMs` elapses first.
|
|
38745
|
+
*/
|
|
38746
|
+
waitForResult(id, timeoutMs) {
|
|
38747
|
+
const existing = this.results.get(id);
|
|
38748
|
+
if (existing) return Promise.resolve(existing);
|
|
38749
|
+
return new Promise((res) => {
|
|
38750
|
+
const timer = setTimeout(() => {
|
|
38751
|
+
this.resultWaiters.get(id)?.delete(waiter);
|
|
38752
|
+
res(null);
|
|
38753
|
+
}, timeoutMs);
|
|
38754
|
+
const waiter = (r) => {
|
|
38755
|
+
clearTimeout(timer);
|
|
38756
|
+
res(r);
|
|
38757
|
+
};
|
|
38758
|
+
if (!this.resultWaiters.has(id)) this.resultWaiters.set(id, /* @__PURE__ */ new Set());
|
|
38759
|
+
this.resultWaiters.get(id)?.add(waiter);
|
|
38760
|
+
});
|
|
38689
38761
|
}
|
|
38690
|
-
|
|
38691
|
-
|
|
38692
|
-
|
|
38693
|
-
|
|
38694
|
-
|
|
38695
|
-
|
|
38696
|
-
}
|
|
38697
|
-
var ProjectContext = class {
|
|
38698
|
-
config;
|
|
38699
|
-
chainId;
|
|
38700
|
-
deployment;
|
|
38701
|
-
contracts;
|
|
38702
|
-
constructor() {
|
|
38703
|
-
const cfg = readJsonFile(sailPath("config.json"));
|
|
38704
|
-
if (!cfg) {
|
|
38705
|
-
throw new Error('No Sailor project found here. Run "sailor init" first.');
|
|
38762
|
+
/** Push a signing request to the UI and await the user's response (in-process). */
|
|
38763
|
+
async requestSignature(req, timeoutMs = 10 * 60 * 1e3) {
|
|
38764
|
+
const request = this.enqueue(req, timeoutMs);
|
|
38765
|
+
const result = await this.waitForResult(request.id, timeoutMs + 2e3);
|
|
38766
|
+
if (!result) {
|
|
38767
|
+
throw new Error(`Signing request "${request.title}" timed out after ${timeoutMs / 1e3}s`);
|
|
38706
38768
|
}
|
|
38707
|
-
|
|
38708
|
-
this.chainId = cfg.chainId ?? 8453;
|
|
38709
|
-
this.deployment = getSailDeployment(this.chainId);
|
|
38710
|
-
const overrides = cfg.contracts ?? {};
|
|
38711
|
-
this.contracts = {
|
|
38712
|
-
chainId: this.chainId,
|
|
38713
|
-
kernel: getAddress(nonEmpty(overrides.kernel) ? overrides.kernel : this.deployment.kernel),
|
|
38714
|
-
governance: getAddress(
|
|
38715
|
-
nonEmpty(overrides.governance) ? overrides.governance : this.deployment.governance
|
|
38716
|
-
),
|
|
38717
|
-
standardFeePolicy: getAddress(
|
|
38718
|
-
nonEmpty(overrides.standardFeePolicy) ? overrides.standardFeePolicy : this.deployment.standardFeePolicy
|
|
38719
|
-
),
|
|
38720
|
-
safeModuleEnabler: getAddress(
|
|
38721
|
-
nonEmpty(overrides.safeModuleEnabler) ? overrides.safeModuleEnabler : this.deployment.safeModuleEnabler
|
|
38722
|
-
),
|
|
38723
|
-
permissionFactory: getAddress(
|
|
38724
|
-
nonEmpty(overrides.permissionFactory) ? overrides.permissionFactory : this.deployment.permissionFactory
|
|
38725
|
-
)
|
|
38726
|
-
};
|
|
38727
|
-
}
|
|
38728
|
-
static exists() {
|
|
38729
|
-
return fileExists(sailPath("config.json"));
|
|
38730
|
-
}
|
|
38731
|
-
get name() {
|
|
38732
|
-
return this.config.name ?? "sailor-agent";
|
|
38733
|
-
}
|
|
38734
|
-
// ── Owner persistence (.sail/state/owner.json) ──────────────────────────────
|
|
38735
|
-
getOwner() {
|
|
38736
|
-
const state = readJsonFile(sailPath("state", "owner.json"));
|
|
38737
|
-
if (state?.owner) return getAddress(state.owner);
|
|
38738
|
-
const account2 = readJsonFile(sailPath("account.json"));
|
|
38739
|
-
return account2?.owner ? getAddress(account2.owner) : null;
|
|
38769
|
+
return result;
|
|
38740
38770
|
}
|
|
38741
|
-
|
|
38742
|
-
|
|
38743
|
-
|
|
38744
|
-
|
|
38745
|
-
|
|
38746
|
-
|
|
38771
|
+
recordResult(response, request) {
|
|
38772
|
+
const id = response.requestId;
|
|
38773
|
+
if (this.results.has(id)) return;
|
|
38774
|
+
this.results.set(id, response);
|
|
38775
|
+
const waiters2 = this.resultWaiters.get(id);
|
|
38776
|
+
if (waiters2) {
|
|
38777
|
+
for (const w of waiters2) w(response);
|
|
38778
|
+
this.resultWaiters.delete(id);
|
|
38779
|
+
}
|
|
38780
|
+
this.broadcast({ type: "request-resolved", requestId: id });
|
|
38781
|
+
setTimeout(() => this.results.delete(id), 10 * 60 * 1e3).unref?.();
|
|
38782
|
+
this.logOwnerActivity(response, request);
|
|
38747
38783
|
}
|
|
38748
|
-
|
|
38749
|
-
|
|
38750
|
-
|
|
38784
|
+
/**
|
|
38785
|
+
* Append the owner's signing decision to the unified activity log. This is
|
|
38786
|
+
* the single place every owner action lands — whether the request was
|
|
38787
|
+
* approved (a signed tx or an off-chain EIP-712 signature) or rejected — so
|
|
38788
|
+
* the dashboard's Recent Activity can show what the owner did, alongside the
|
|
38789
|
+
* agent's dispatches. We only log when the originating request is known
|
|
38790
|
+
* (its `kind`/`title` give the event meaning); a bare result with no request
|
|
38791
|
+
* carries nothing worth showing.
|
|
38792
|
+
*/
|
|
38793
|
+
logOwnerActivity(response, request) {
|
|
38794
|
+
if (!request) return;
|
|
38795
|
+
const base2 = {
|
|
38796
|
+
ts: nowIso(),
|
|
38797
|
+
actor: "owner",
|
|
38798
|
+
kind: request.kind,
|
|
38799
|
+
title: request.title,
|
|
38800
|
+
chainId: request.chainId
|
|
38801
|
+
};
|
|
38802
|
+
let event;
|
|
38803
|
+
if (response.status === "signed") {
|
|
38804
|
+
event = { ...base2, type: "owner_signed", txHash: response.txHash };
|
|
38805
|
+
} else if (response.status === "signature") {
|
|
38806
|
+
event = { ...base2, type: "owner_signed", offchain: true };
|
|
38807
|
+
} else {
|
|
38808
|
+
event = { ...base2, type: "owner_rejected", reason: response.reason };
|
|
38809
|
+
}
|
|
38751
38810
|
try {
|
|
38752
|
-
|
|
38753
|
-
if (env.SAIL_PASSPHRASE) process.env.SAIL_PASSPHRASE = env.SAIL_PASSPHRASE;
|
|
38811
|
+
appendActivity(event, (0, import_node_path4.join)(this.projectRoot, ".sail"));
|
|
38754
38812
|
} catch {
|
|
38755
38813
|
}
|
|
38756
38814
|
}
|
|
38757
|
-
|
|
38758
|
-
|
|
38759
|
-
|
|
38760
|
-
if (!keystore) {
|
|
38761
|
-
throw new Error('No manager key found.\nRun "sailor keys generate" and choose "manager".');
|
|
38762
|
-
}
|
|
38763
|
-
return LocalKeyring.fromKeystore(keystore, passphrase);
|
|
38815
|
+
/** Path to `<projectRoot>/.sail/<...segments>`. */
|
|
38816
|
+
sailFile(...segments) {
|
|
38817
|
+
return (0, import_node_path4.join)(this.projectRoot, ".sail", ...segments);
|
|
38764
38818
|
}
|
|
38765
|
-
|
|
38766
|
-
|
|
38767
|
-
|
|
38768
|
-
|
|
38769
|
-
|
|
38770
|
-
|
|
38771
|
-
|
|
38772
|
-
|
|
38773
|
-
|
|
38819
|
+
/** Stream a JSON file back, or a fallback body when it is missing/invalid. */
|
|
38820
|
+
sendJsonFile(res, filePath, fallback2) {
|
|
38821
|
+
try {
|
|
38822
|
+
const raw = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
38823
|
+
JSON.parse(raw);
|
|
38824
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38825
|
+
res.end(raw);
|
|
38826
|
+
} catch {
|
|
38827
|
+
res.writeHead(fallback2.status, { "Content-Type": "application/json" });
|
|
38828
|
+
res.end(JSON.stringify(fallback2.body));
|
|
38829
|
+
}
|
|
38774
38830
|
}
|
|
38775
|
-
|
|
38776
|
-
|
|
38777
|
-
|
|
38778
|
-
|
|
38779
|
-
|
|
38780
|
-
|
|
38781
|
-
|
|
38782
|
-
|
|
38783
|
-
|
|
38784
|
-
|
|
38785
|
-
|
|
38786
|
-
|
|
38787
|
-
|
|
38788
|
-
|
|
38831
|
+
/**
|
|
38832
|
+
* Persist a Safe deployed/imported from the dashboard. Mirrors the UI data
|
|
38833
|
+
* server's `POST /api/account` (packages/ui/server.js): upsert the SMA into
|
|
38834
|
+
* `state/accounts.json` (so the account switcher and the agent see it) BEFORE
|
|
38835
|
+
* overwriting `account.json` with the new active SMA — the upsert backfills
|
|
38836
|
+
* from the previously-active account.json, so writing it first would drop the
|
|
38837
|
+
* prior SMA.
|
|
38838
|
+
*/
|
|
38839
|
+
handleSaveAccount(req, res) {
|
|
38840
|
+
this.readBody(req).then((body) => {
|
|
38841
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
38842
|
+
const { safe, owner: owner2, permissionSigner, manager, chainId, createdAtBlock } = parsed;
|
|
38843
|
+
if (!safe || !owner2 || !chainId) {
|
|
38844
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
38845
|
+
res.end(JSON.stringify({ error: "safe, owner, and chainId are required" }));
|
|
38846
|
+
return;
|
|
38847
|
+
}
|
|
38848
|
+
const record = {
|
|
38849
|
+
safe,
|
|
38850
|
+
owner: owner2,
|
|
38851
|
+
permissionSigner: permissionSigner ?? owner2,
|
|
38852
|
+
manager: manager ?? owner2,
|
|
38853
|
+
chainId,
|
|
38854
|
+
createdAtBlock: createdAtBlock ?? "0"
|
|
38855
|
+
};
|
|
38856
|
+
const baseSailDir = this.sailFile();
|
|
38857
|
+
upsertAccountInList(record, void 0, baseSailDir);
|
|
38858
|
+
(0, import_node_fs5.mkdirSync)(baseSailDir, { recursive: true });
|
|
38859
|
+
(0, import_node_fs5.writeFileSync)(this.sailFile("account.json"), `${JSON.stringify(record, null, 2)}
|
|
38860
|
+
`);
|
|
38861
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38862
|
+
res.end(JSON.stringify({ ok: true }));
|
|
38863
|
+
}).catch((err) => {
|
|
38864
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
38865
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
38866
|
+
});
|
|
38789
38867
|
}
|
|
38790
|
-
|
|
38791
|
-
|
|
38792
|
-
|
|
38793
|
-
|
|
38794
|
-
|
|
38795
|
-
|
|
38796
|
-
|
|
38797
|
-
}));
|
|
38798
|
-
const knownTemplates = (deployment.knownTemplates ?? []).map((t) => ({
|
|
38799
|
-
kind: t.kind,
|
|
38800
|
-
label: t.label,
|
|
38801
|
-
description: t.description,
|
|
38802
|
-
address: t.address
|
|
38803
|
-
}));
|
|
38804
|
-
const bareTemplates = Object.keys(deployment.standaloneTemplates ?? {}).filter(
|
|
38805
|
-
(k) => !cloneTemplates.some((c) => c.key === k)
|
|
38806
|
-
);
|
|
38807
|
-
const strategyPrimitives = [
|
|
38808
|
-
"strategy.swap \u2014 bounded swap (one-off, or looped on a schedule for DCA/rebalance)",
|
|
38809
|
-
"dispatch.single \u2014 a single permitted call through the kernel",
|
|
38810
|
-
dispatchModel === "selective" ? "dispatch.batch / dispatch.preview \u2014 multi-call (selective kernels only)" : "dispatch.batch / dispatch.preview \u2014 UNAVAILABLE on this conjunctive kernel"
|
|
38811
|
-
];
|
|
38812
|
-
const payload = {
|
|
38813
|
-
chainId,
|
|
38814
|
-
chainName: chainName(chainId),
|
|
38815
|
-
supported: true,
|
|
38816
|
-
dispatchModel,
|
|
38817
|
-
dispatchModelSource: modelSource,
|
|
38818
|
-
contracts: {
|
|
38819
|
-
kernel,
|
|
38820
|
-
permissionFactory: project.contracts.permissionFactory
|
|
38821
|
-
},
|
|
38822
|
-
supportedChains: Object.keys(sailDeployments).map((id) => ({
|
|
38823
|
-
chainId: Number(id),
|
|
38824
|
-
name: chainName(Number(id)),
|
|
38825
|
-
dispatchModel: sailDeployments[id].dispatchModel
|
|
38826
|
-
})),
|
|
38827
|
-
mandateTemplates: {
|
|
38828
|
-
// No-Solidity, self-describing clone templates (deployAndAttach + initialize).
|
|
38829
|
-
cloneTemplates,
|
|
38830
|
-
// Pre-deployed shared permissions.
|
|
38831
|
-
knownTemplates,
|
|
38832
|
-
// Deployable clone logic without rich wizard metadata yet.
|
|
38833
|
-
otherStandaloneTemplates: bareTemplates
|
|
38834
|
-
},
|
|
38835
|
-
strategyPrimitives,
|
|
38836
|
-
customMandates: "Author a Foundry IPermission contract under mandates/ when no template fits; keep all policy parameters constructor-configured.",
|
|
38837
|
-
intelligence: {
|
|
38838
|
-
baseUrl: SAIL_INTELLIGENCE_BASE_URL,
|
|
38839
|
-
docsUrl: SAIL_INTELLIGENCE_DOCS_URL,
|
|
38840
|
-
use: "Vault screening, allocation, and rebalance advice for yield strategies."
|
|
38868
|
+
/** All known SMAs, annotating the currently-active one (mirrors the UI server). */
|
|
38869
|
+
handleListAccounts(res) {
|
|
38870
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38871
|
+
let active = null;
|
|
38872
|
+
try {
|
|
38873
|
+
active = JSON.parse((0, import_node_fs5.readFileSync)(this.sailFile("account.json"), "utf-8")).safe;
|
|
38874
|
+
} catch {
|
|
38841
38875
|
}
|
|
38842
|
-
|
|
38843
|
-
|
|
38844
|
-
|
|
38845
|
-
() => {
|
|
38846
|
-
console.log("Sailor capabilities");
|
|
38847
|
-
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
38848
|
-
console.log(`Chain: ${payload.chainName} (${chainId})`);
|
|
38849
|
-
console.log(`Dispatch model: ${dispatchModel ?? "unknown"} (${modelSource})`);
|
|
38850
|
-
console.log(
|
|
38851
|
-
`Supported chains: ${payload.supportedChains.map((c) => `${c.name} [${c.dispatchModel}]`).join(", ")}`
|
|
38876
|
+
try {
|
|
38877
|
+
const accounts = JSON.parse(
|
|
38878
|
+
(0, import_node_fs5.readFileSync)(this.sailFile("state", "accounts.json"), "utf-8")
|
|
38852
38879
|
);
|
|
38853
|
-
|
|
38854
|
-
|
|
38855
|
-
|
|
38856
|
-
|
|
38857
|
-
|
|
38858
|
-
|
|
38859
|
-
|
|
38860
|
-
}
|
|
38861
|
-
for (const t of knownTemplates) {
|
|
38862
|
-
console.log(` \u2022 ${t.label} (${t.kind}, shared) \u2014 ${t.description ?? ""}`);
|
|
38863
|
-
}
|
|
38864
|
-
if (bareTemplates.length > 0) {
|
|
38865
|
-
console.log(` \u2022 also deployable: ${bareTemplates.join(", ")}`);
|
|
38866
|
-
}
|
|
38867
|
-
console.log("\nStrategy primitives:");
|
|
38868
|
-
for (const p of strategyPrimitives) console.log(` \u2022 ${p}`);
|
|
38869
|
-
console.log("\nCustom mandates:");
|
|
38870
|
-
console.log(` ${payload.customMandates}`);
|
|
38871
|
-
console.log("\nIntelligence API (yield/allocation advice):");
|
|
38872
|
-
console.log(` ${SAIL_INTELLIGENCE_BASE_URL} (docs: ${SAIL_INTELLIGENCE_DOCS_URL})`);
|
|
38873
|
-
console.log(
|
|
38874
|
-
"\nUse this to decide if a request is buildable. If it can't be expressed as a template, a strategy primitive, or a custom mandate, say so \u2014 don't scaffold a revert."
|
|
38880
|
+
res.end(
|
|
38881
|
+
JSON.stringify(
|
|
38882
|
+
accounts.map((a) => ({
|
|
38883
|
+
...a,
|
|
38884
|
+
active: a.safe.toLowerCase() === active?.toLowerCase()
|
|
38885
|
+
}))
|
|
38886
|
+
)
|
|
38875
38887
|
);
|
|
38876
|
-
}
|
|
38877
|
-
|
|
38878
|
-
|
|
38879
|
-
|
|
38880
|
-
|
|
38881
|
-
|
|
38882
|
-
|
|
38883
|
-
|
|
38884
|
-
// src/lib/contract-check.ts
|
|
38885
|
-
async function checkContractExists(pc, address) {
|
|
38886
|
-
try {
|
|
38887
|
-
const code = await pc.getCode({ address });
|
|
38888
|
-
return { address, hasCode: !!code && code !== "0x" };
|
|
38889
|
-
} catch (err) {
|
|
38890
|
-
return { address, hasCode: false, error: err.message.split("\n")[0] };
|
|
38891
|
-
}
|
|
38892
|
-
}
|
|
38893
|
-
|
|
38894
|
-
// src/lib/permission-resolver.ts
|
|
38895
|
-
var IPERMISSION_ABI = [
|
|
38896
|
-
{
|
|
38897
|
-
type: "function",
|
|
38898
|
-
name: "evaluate",
|
|
38899
|
-
stateMutability: "view",
|
|
38900
|
-
inputs: [
|
|
38901
|
-
{ name: "txData", type: "bytes" },
|
|
38902
|
-
{
|
|
38903
|
-
name: "ctx",
|
|
38904
|
-
type: "tuple",
|
|
38905
|
-
components: [
|
|
38906
|
-
{ name: "account", type: "address" },
|
|
38907
|
-
{ name: "manager", type: "address" },
|
|
38908
|
-
{ name: "submitter", type: "address" },
|
|
38909
|
-
{ name: "target", type: "address" },
|
|
38910
|
-
{ name: "selector", type: "bytes4" },
|
|
38911
|
-
{ name: "value", type: "uint256" },
|
|
38912
|
-
{ name: "blockTimestamp", type: "uint256" },
|
|
38913
|
-
{ name: "blockNumber", type: "uint256" }
|
|
38914
|
-
]
|
|
38888
|
+
} catch {
|
|
38889
|
+
try {
|
|
38890
|
+
const a = JSON.parse(
|
|
38891
|
+
(0, import_node_fs5.readFileSync)(this.sailFile("account.json"), "utf-8")
|
|
38892
|
+
);
|
|
38893
|
+
res.end(JSON.stringify([{ ...a, name: "My SMA", active: true, addedAt: null }]));
|
|
38894
|
+
} catch {
|
|
38895
|
+
res.end("[]");
|
|
38915
38896
|
}
|
|
38916
|
-
|
|
38917
|
-
outputs: [{ type: "bool" }]
|
|
38918
|
-
}
|
|
38919
|
-
];
|
|
38920
|
-
function buildPermissionContext(params) {
|
|
38921
|
-
const { account: account2, manager, call: call2, blockInfo } = params;
|
|
38922
|
-
const selector = call2.data.length >= 10 ? call2.data.slice(0, 10) : "0x00000000";
|
|
38923
|
-
return {
|
|
38924
|
-
account: account2,
|
|
38925
|
-
manager,
|
|
38926
|
-
submitter: manager,
|
|
38927
|
-
// runner submits dispatches from the manager (agent) wallet
|
|
38928
|
-
target: call2.target,
|
|
38929
|
-
selector,
|
|
38930
|
-
value: call2.value,
|
|
38931
|
-
blockTimestamp: blockInfo.timestamp,
|
|
38932
|
-
blockNumber: blockInfo.number
|
|
38933
|
-
};
|
|
38934
|
-
}
|
|
38935
|
-
async function probePermissionForCall(params) {
|
|
38936
|
-
const { publicClient, permission, account: account2, manager, call: call2, blockInfo } = params;
|
|
38937
|
-
const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
|
|
38938
|
-
try {
|
|
38939
|
-
const accepted = await publicClient.readContract({
|
|
38940
|
-
address: permission,
|
|
38941
|
-
abi: IPERMISSION_ABI,
|
|
38942
|
-
functionName: "evaluate",
|
|
38943
|
-
args: [call2.data, ctx]
|
|
38944
|
-
});
|
|
38945
|
-
return { accepted: Boolean(accepted), reverted: false };
|
|
38946
|
-
} catch (err) {
|
|
38947
|
-
return { accepted: false, reverted: true, error: err.message.split("\n")[0] };
|
|
38897
|
+
}
|
|
38948
38898
|
}
|
|
38949
|
-
|
|
38950
|
-
|
|
38951
|
-
|
|
38952
|
-
|
|
38953
|
-
|
|
38954
|
-
|
|
38955
|
-
|
|
38956
|
-
|
|
38957
|
-
|
|
38958
|
-
|
|
38959
|
-
|
|
38899
|
+
handleHttp(req, res) {
|
|
38900
|
+
const origin = req.headers.origin;
|
|
38901
|
+
const url0 = (req.url ?? "/").split("?")[0];
|
|
38902
|
+
const isDiscoveryEndpoint = url0 === "/config";
|
|
38903
|
+
const allowedOrigin = isDiscoveryEndpoint && origin?.startsWith("http://localhost:") ? origin : origin === this._url ? origin : this._url;
|
|
38904
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
38905
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
38906
|
+
res.setHeader("Access-Control-Allow-Headers", `Content-Type, ${REQUEST_SECRET_HEADER}`);
|
|
38907
|
+
res.setHeader("Vary", "Origin");
|
|
38908
|
+
if (req.method === "OPTIONS") {
|
|
38909
|
+
res.writeHead(204);
|
|
38910
|
+
res.end();
|
|
38911
|
+
return;
|
|
38912
|
+
}
|
|
38913
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
38914
|
+
if (url === "/config") {
|
|
38915
|
+
const isTrustedOrigin = !origin || origin === this._url;
|
|
38916
|
+
const wsUrlForClient = isTrustedOrigin ? `${this.wsUrl}?secret=${encodeURIComponent(this.requestSecret)}` : this.wsUrl;
|
|
38917
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38918
|
+
res.end(
|
|
38919
|
+
JSON.stringify({
|
|
38920
|
+
url: this._url,
|
|
38921
|
+
wsUrl: wsUrlForClient,
|
|
38922
|
+
port: this.port,
|
|
38923
|
+
pid: process.pid,
|
|
38924
|
+
pendingCount: this.pending.size
|
|
38925
|
+
})
|
|
38926
|
+
);
|
|
38927
|
+
return;
|
|
38928
|
+
}
|
|
38929
|
+
const secretHeader = req.headers[REQUEST_SECRET_HEADER];
|
|
38930
|
+
const isAuthenticated = secretHeader === this.requestSecret;
|
|
38931
|
+
if (url === "/pending") {
|
|
38932
|
+
if (!isAuthenticated) {
|
|
38933
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
38934
|
+
res.end(JSON.stringify({ error: "forbidden" }));
|
|
38935
|
+
return;
|
|
38936
|
+
}
|
|
38937
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38938
|
+
res.end(JSON.stringify(Array.from(this.pending.values()).map((e) => e.request)));
|
|
38939
|
+
return;
|
|
38940
|
+
}
|
|
38941
|
+
if (url === "/wallet") {
|
|
38942
|
+
if (!isAuthenticated) {
|
|
38943
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
38944
|
+
res.end(JSON.stringify({ error: "forbidden" }));
|
|
38945
|
+
return;
|
|
38946
|
+
}
|
|
38947
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38948
|
+
res.end(JSON.stringify({ address: this._connectedWallet ?? null }));
|
|
38949
|
+
return;
|
|
38950
|
+
}
|
|
38951
|
+
if (url === "/requests" && req.method === "POST") {
|
|
38952
|
+
const supplied = req.headers[REQUEST_SECRET_HEADER];
|
|
38953
|
+
if (supplied !== this.requestSecret) {
|
|
38954
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
38955
|
+
res.end(JSON.stringify({ error: "forbidden" }));
|
|
38956
|
+
return;
|
|
38957
|
+
}
|
|
38958
|
+
this.readBody(req).then((body) => {
|
|
38959
|
+
const parsed = JSON.parse(body);
|
|
38960
|
+
if (!parsed.kind || !["create-sma", "deploy-mandate", "register-permission", "attach-mandate", "revoke-permissions", "set-delegate", "arbitrary-tx"].includes(parsed.kind)) {
|
|
38961
|
+
throw new Error(`Unknown signing request kind: ${String(parsed.kind)}`);
|
|
38962
|
+
}
|
|
38963
|
+
const request = this.enqueue(parsed);
|
|
38964
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38965
|
+
res.end(JSON.stringify({ id: request.id }));
|
|
38966
|
+
}).catch((err) => {
|
|
38967
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
38968
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
38960
38969
|
});
|
|
38961
|
-
|
|
38962
|
-
} catch {
|
|
38970
|
+
return;
|
|
38963
38971
|
}
|
|
38964
|
-
|
|
38965
|
-
|
|
38966
|
-
|
|
38967
|
-
|
|
38968
|
-
|
|
38969
|
-
|
|
38970
|
-
|
|
38971
|
-
|
|
38972
|
-
address: kernel,
|
|
38973
|
-
abi: SailKernelAbi,
|
|
38974
|
-
functionName: "previewBatch",
|
|
38975
|
-
args: [account2, permission, calls]
|
|
38972
|
+
if (url === "/api/account" && req.method === "POST") {
|
|
38973
|
+
this.handleSaveAccount(req, res);
|
|
38974
|
+
return;
|
|
38975
|
+
}
|
|
38976
|
+
if (url === "/api/account" && (req.method === "GET" || req.method == null)) {
|
|
38977
|
+
this.sendJsonFile(res, (0, import_node_path4.join)(this.projectRoot, ".sail", "account.json"), {
|
|
38978
|
+
status: 404,
|
|
38979
|
+
body: { error: "account not found" }
|
|
38976
38980
|
});
|
|
38977
|
-
|
|
38978
|
-
} catch {
|
|
38981
|
+
return;
|
|
38979
38982
|
}
|
|
38980
|
-
|
|
38981
|
-
|
|
38982
|
-
|
|
38983
|
-
|
|
38984
|
-
|
|
38985
|
-
|
|
38986
|
-
|
|
38987
|
-
|
|
38988
|
-
|
|
38989
|
-
|
|
38990
|
-
|
|
38991
|
-
|
|
38992
|
-
|
|
38993
|
-
|
|
38994
|
-
|
|
38995
|
-
|
|
38996
|
-
|
|
38997
|
-
|
|
38998
|
-
|
|
38999
|
-
}
|
|
39000
|
-
var PROBE_TARGET = "0x000000000000000000000000000000000000dEaD";
|
|
39001
|
-
var PROBE_SELECTOR = "0xffffffff";
|
|
39002
|
-
var PROBE_DATA = "0xffffffff";
|
|
39003
|
-
async function probePassThrough(pc, permission, account2) {
|
|
39004
|
-
try {
|
|
39005
|
-
const ok = await pc.readContract({
|
|
39006
|
-
address: permission,
|
|
39007
|
-
abi: IPERMISSION_ABI,
|
|
39008
|
-
functionName: "evaluate",
|
|
39009
|
-
args: [
|
|
39010
|
-
PROBE_DATA,
|
|
39011
|
-
{
|
|
39012
|
-
account: account2,
|
|
39013
|
-
manager: account2,
|
|
39014
|
-
submitter: account2,
|
|
39015
|
-
target: PROBE_TARGET,
|
|
39016
|
-
selector: PROBE_SELECTOR,
|
|
39017
|
-
value: 0n,
|
|
39018
|
-
blockTimestamp: 0n,
|
|
39019
|
-
blockNumber: 0n
|
|
38983
|
+
if (url === "/api/accounts" && (req.method === "GET" || req.method == null)) {
|
|
38984
|
+
this.handleListAccounts(res);
|
|
38985
|
+
return;
|
|
38986
|
+
}
|
|
38987
|
+
const resultMatch = url.match(/^\/requests\/([^/]+)\/result$/);
|
|
38988
|
+
if (resultMatch && (req.method === "GET" || req.method == null)) {
|
|
38989
|
+
if (!isAuthenticated) {
|
|
38990
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
38991
|
+
res.end(JSON.stringify({ error: "forbidden" }));
|
|
38992
|
+
return;
|
|
38993
|
+
}
|
|
38994
|
+
const id = decodeURIComponent(resultMatch[1]);
|
|
38995
|
+
this.waitForResult(id, RESULT_LONGPOLL_MS).then((result) => {
|
|
38996
|
+
if (result) {
|
|
38997
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38998
|
+
res.end(JSON.stringify(result));
|
|
38999
|
+
} else {
|
|
39000
|
+
res.writeHead(204);
|
|
39001
|
+
res.end();
|
|
39020
39002
|
}
|
|
39021
|
-
|
|
39022
|
-
|
|
39023
|
-
|
|
39024
|
-
|
|
39025
|
-
|
|
39026
|
-
|
|
39027
|
-
|
|
39028
|
-
|
|
39003
|
+
});
|
|
39004
|
+
return;
|
|
39005
|
+
}
|
|
39006
|
+
if (this.uiDist) {
|
|
39007
|
+
const rawPath = (req.url ?? "/").split("?")[0];
|
|
39008
|
+
const filePath = (0, import_node_path4.resolve)((0, import_node_path4.join)(this.uiDist, rawPath === "/" ? "index.html" : rawPath));
|
|
39009
|
+
if (!filePath.startsWith((0, import_node_path4.resolve)(this.uiDist))) {
|
|
39010
|
+
res.writeHead(403);
|
|
39011
|
+
res.end();
|
|
39012
|
+
return;
|
|
39013
|
+
}
|
|
39014
|
+
if ((0, import_node_fs5.existsSync)(filePath)) {
|
|
39015
|
+
const mime = MIME[(0, import_node_path4.extname)(filePath)] ?? "application/octet-stream";
|
|
39016
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
39017
|
+
res.end((0, import_node_fs5.readFileSync)(filePath));
|
|
39018
|
+
return;
|
|
39019
|
+
}
|
|
39020
|
+
const indexHtml = (0, import_node_path4.join)(this.uiDist, "index.html");
|
|
39021
|
+
if ((0, import_node_fs5.existsSync)(indexHtml)) {
|
|
39022
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
39023
|
+
res.end((0, import_node_fs5.readFileSync)(indexHtml));
|
|
39024
|
+
return;
|
|
39025
|
+
}
|
|
39026
|
+
}
|
|
39027
|
+
res.writeHead(404);
|
|
39028
|
+
res.end();
|
|
39029
|
+
}
|
|
39030
|
+
handleConnection(ws) {
|
|
39031
|
+
this.clients.add(ws);
|
|
39032
|
+
const msg = {
|
|
39033
|
+
type: "pending",
|
|
39034
|
+
requests: Array.from(this.pending.values()).map((e) => e.request)
|
|
39029
39035
|
};
|
|
39036
|
+
ws.send(JSON.stringify(msg));
|
|
39037
|
+
ws.on("message", (data) => {
|
|
39038
|
+
try {
|
|
39039
|
+
const parsed = JSON.parse(data.toString());
|
|
39040
|
+
this.handleClientMessage(ws, parsed);
|
|
39041
|
+
} catch {
|
|
39042
|
+
}
|
|
39043
|
+
});
|
|
39044
|
+
ws.on("close", () => this.clients.delete(ws));
|
|
39045
|
+
ws.on("error", () => this.clients.delete(ws));
|
|
39030
39046
|
}
|
|
39031
|
-
|
|
39032
|
-
|
|
39033
|
-
|
|
39034
|
-
|
|
39035
|
-
|
|
39036
|
-
|
|
39037
|
-
|
|
39038
|
-
const pc = createPublicClient({ chain: getChainById(chainId), transport: http(rpcUrl) });
|
|
39039
|
-
const caps = await client.capabilities();
|
|
39040
|
-
const stored = readJsonFile(sailPath("account.json"));
|
|
39041
|
-
const safe = options.account ? getAddress(options.account) : stored?.safe ? getAddress(stored.safe) : null;
|
|
39042
|
-
let permissions = [];
|
|
39043
|
-
let checks = [];
|
|
39044
|
-
let permsNoCode = [];
|
|
39045
|
-
if (safe) {
|
|
39046
|
-
const mandates = await client.mandate.list(safe);
|
|
39047
|
-
permissions = mandates.map((m) => getAddress(m.permission));
|
|
39048
|
-
if (permissions.length > 0) {
|
|
39049
|
-
const codeChecks = await Promise.all(permissions.map((p) => checkContractExists(pc, p)));
|
|
39050
|
-
permsNoCode = codeChecks.filter((c) => !c.hasCode && !c.error).map((c) => c.address);
|
|
39047
|
+
handleClientMessage(_ws, msg) {
|
|
39048
|
+
if (msg.type === "wallet-connected") {
|
|
39049
|
+
if (typeof msg.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(msg.address)) return;
|
|
39050
|
+
this._connectedWallet = msg.address;
|
|
39051
|
+
for (const listener of this.walletListeners) listener(msg.address);
|
|
39052
|
+
this.walletListeners = [];
|
|
39053
|
+
return;
|
|
39051
39054
|
}
|
|
39052
|
-
if (
|
|
39053
|
-
|
|
39055
|
+
if (msg.type === "wallet-disconnected") {
|
|
39056
|
+
this._connectedWallet = void 0;
|
|
39057
|
+
return;
|
|
39058
|
+
}
|
|
39059
|
+
const entry = this.pending.get(msg.requestId);
|
|
39060
|
+
if (!entry) return;
|
|
39061
|
+
clearTimeout(entry.timer);
|
|
39062
|
+
this.pending.delete(msg.requestId);
|
|
39063
|
+
const { request } = entry;
|
|
39064
|
+
if (msg.type === "signed") {
|
|
39065
|
+
this.recordResult(
|
|
39066
|
+
{ status: "signed", requestId: msg.requestId, txHash: msg.txHash },
|
|
39067
|
+
request
|
|
39068
|
+
);
|
|
39069
|
+
} else if (msg.type === "signature") {
|
|
39070
|
+
this.recordResult(
|
|
39071
|
+
{ status: "signature", requestId: msg.requestId, signature: msg.signature },
|
|
39072
|
+
request
|
|
39073
|
+
);
|
|
39074
|
+
} else {
|
|
39075
|
+
this.recordResult(
|
|
39076
|
+
{
|
|
39077
|
+
status: "rejected",
|
|
39078
|
+
requestId: msg.requestId,
|
|
39079
|
+
reason: msg.reason
|
|
39080
|
+
},
|
|
39081
|
+
request
|
|
39082
|
+
);
|
|
39054
39083
|
}
|
|
39055
39084
|
}
|
|
39056
|
-
|
|
39057
|
-
|
|
39058
|
-
|
|
39059
|
-
|
|
39060
|
-
|
|
39061
|
-
|
|
39062
|
-
|
|
39063
|
-
|
|
39085
|
+
readBody(req, maxBytes = 1e6) {
|
|
39086
|
+
return new Promise((res, rej) => {
|
|
39087
|
+
let size5 = 0;
|
|
39088
|
+
const chunks = [];
|
|
39089
|
+
req.on("data", (c) => {
|
|
39090
|
+
size5 += c.length;
|
|
39091
|
+
if (size5 > maxBytes) {
|
|
39092
|
+
rej(new Error("Request body too large"));
|
|
39093
|
+
req.destroy();
|
|
39094
|
+
return;
|
|
39095
|
+
}
|
|
39096
|
+
chunks.push(c);
|
|
39097
|
+
});
|
|
39098
|
+
req.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
|
|
39099
|
+
req.on("error", rej);
|
|
39100
|
+
});
|
|
39064
39101
|
}
|
|
39065
|
-
|
|
39066
|
-
|
|
39067
|
-
|
|
39068
|
-
|
|
39069
|
-
|
|
39070
|
-
if (managerAddr) managerBal = await nativeBalance(pc, managerAddr);
|
|
39071
|
-
} catch {
|
|
39102
|
+
broadcast(msg) {
|
|
39103
|
+
const data = JSON.stringify(msg);
|
|
39104
|
+
for (const ws of this.clients) {
|
|
39105
|
+
if (ws.readyState === import_websocket2.default.OPEN) ws.send(data);
|
|
39106
|
+
}
|
|
39072
39107
|
}
|
|
39073
|
-
|
|
39074
|
-
|
|
39108
|
+
writeRuntimeState() {
|
|
39109
|
+
if (!(0, import_node_fs5.existsSync)(this.runtimeDir)) (0, import_node_fs5.mkdirSync)(this.runtimeDir, { recursive: true });
|
|
39110
|
+
(0, import_node_fs5.writeFileSync)(
|
|
39111
|
+
(0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE),
|
|
39075
39112
|
JSON.stringify(
|
|
39076
39113
|
{
|
|
39077
|
-
|
|
39078
|
-
|
|
39079
|
-
|
|
39080
|
-
|
|
39081
|
-
|
|
39082
|
-
|
|
39083
|
-
wallet: {
|
|
39084
|
-
owner: ownerAddr ? { address: ownerAddr, ...ownerBal ?? {} } : null,
|
|
39085
|
-
manager: managerAddr ? { address: managerAddr, ...managerBal ?? {} } : null
|
|
39086
|
-
},
|
|
39087
|
-
account: safe,
|
|
39088
|
-
saltNonce: stored?.saltNonce ?? null,
|
|
39089
|
-
permissions,
|
|
39090
|
-
permissionsWithoutCode: permsNoCode,
|
|
39091
|
-
conjunctivePassThrough: caps.dispatchModel === "conjunctive" ? checks.map((c) => ({
|
|
39092
|
-
permission: c.permission,
|
|
39093
|
-
passesThrough: c.passesThrough,
|
|
39094
|
-
note: c.note
|
|
39095
|
-
})) : "n/a (selective kernel)",
|
|
39096
|
-
healthy
|
|
39114
|
+
url: this._url,
|
|
39115
|
+
wsUrl: this.wsUrl,
|
|
39116
|
+
port: this.port,
|
|
39117
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39118
|
+
pid: process.pid,
|
|
39119
|
+
requestSecret: this.requestSecret
|
|
39097
39120
|
},
|
|
39098
39121
|
null,
|
|
39099
39122
|
2
|
|
39100
39123
|
)
|
|
39101
39124
|
);
|
|
39102
|
-
return;
|
|
39103
39125
|
}
|
|
39104
|
-
|
|
39105
|
-
|
|
39106
|
-
|
|
39107
|
-
|
|
39108
|
-
|
|
39109
|
-
|
|
39110
|
-
console.log(`\u2717 Setup issue \u2014 ${bricking.length} of ${permissions.length} permissions block unrelated calls (see below).`);
|
|
39111
|
-
} else {
|
|
39112
|
-
console.log(
|
|
39113
|
-
"\u2713 Everything looks good \u2014 your SMA is deployed, your permission is registered,\n and your agent is authorized to dispatch."
|
|
39114
|
-
);
|
|
39126
|
+
removeRuntimeState() {
|
|
39127
|
+
const path8 = (0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE);
|
|
39128
|
+
try {
|
|
39129
|
+
if ((0, import_node_fs5.existsSync)(path8)) (0, import_node_fs5.unlinkSync)(path8);
|
|
39130
|
+
} catch {
|
|
39131
|
+
}
|
|
39115
39132
|
}
|
|
39116
|
-
|
|
39117
|
-
|
|
39118
|
-
|
|
39119
|
-
|
|
39120
|
-
|
|
39121
|
-
|
|
39122
|
-
|
|
39123
|
-
|
|
39124
|
-
|
|
39125
|
-
|
|
39126
|
-
|
|
39127
|
-
|
|
39133
|
+
};
|
|
39134
|
+
async function findAvailablePort(startPort) {
|
|
39135
|
+
return new Promise((res) => {
|
|
39136
|
+
const probe = (0, import_node_net.createServer)();
|
|
39137
|
+
probe.listen(startPort, "127.0.0.1", () => {
|
|
39138
|
+
const addr = probe.address();
|
|
39139
|
+
probe.close(() => res(addr.port));
|
|
39140
|
+
});
|
|
39141
|
+
probe.on("error", () => res(findAvailablePort(startPort + 1)));
|
|
39142
|
+
});
|
|
39143
|
+
}
|
|
39144
|
+
|
|
39145
|
+
// src/signing/client.ts
|
|
39146
|
+
var RUNTIME_SERVER_FILE = (0, import_node_path5.join)(".sail", "runtime", "server.json");
|
|
39147
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
39148
|
+
var SigningClient = class {
|
|
39149
|
+
constructor(baseUrl, requestSecret = "") {
|
|
39150
|
+
this.baseUrl = baseUrl;
|
|
39151
|
+
this.requestSecret = requestSecret;
|
|
39128
39152
|
}
|
|
39129
|
-
|
|
39130
|
-
|
|
39131
|
-
|
|
39132
|
-
|
|
39133
|
-
|
|
39134
|
-
if (!
|
|
39135
|
-
|
|
39136
|
-
return;
|
|
39153
|
+
remote = true;
|
|
39154
|
+
get url() {
|
|
39155
|
+
return this.baseUrl;
|
|
39156
|
+
}
|
|
39157
|
+
async start() {
|
|
39158
|
+
if (!await this.ping()) {
|
|
39159
|
+
throw new Error(`Signing station not reachable at ${this.baseUrl}`);
|
|
39137
39160
|
}
|
|
39138
|
-
const flag = !bal.funded ? "\u2717 unfunded" : bal.low ? "\u26A0 low" : "\u2713";
|
|
39139
|
-
console.log(` ${label}: ${addr} ${bal.eth} ETH ${flag}`);
|
|
39140
|
-
};
|
|
39141
|
-
showBalance("owner ", ownerAddr, ownerBal);
|
|
39142
|
-
showBalance("manager", managerAddr, managerBal);
|
|
39143
|
-
if (managerBal && !managerBal.funded) {
|
|
39144
|
-
console.log(
|
|
39145
|
-
' \u2192 The manager (agent) pays gas. Fund it before "sailor run" or dispatches fail.'
|
|
39146
|
-
);
|
|
39147
39161
|
}
|
|
39148
|
-
|
|
39149
|
-
|
|
39150
|
-
console.log("Skipping permission checks.");
|
|
39151
|
-
return;
|
|
39162
|
+
/** No-op: never tear down a daemon the user is running in another process. */
|
|
39163
|
+
stop() {
|
|
39152
39164
|
}
|
|
39153
|
-
|
|
39154
|
-
if (stored?.saltNonce != null) {
|
|
39155
|
-
const saltNonce = BigInt(stored.saltNonce);
|
|
39156
|
-
const MAINNET_CHAINS = [8453, 42161, 130];
|
|
39165
|
+
async ping() {
|
|
39157
39166
|
try {
|
|
39158
|
-
const
|
|
39159
|
-
|
|
39160
|
-
|
|
39161
|
-
|
|
39167
|
+
const r = await fetch(`${this.baseUrl}/config`, { signal: AbortSignal.timeout(1500) });
|
|
39168
|
+
return r.ok;
|
|
39169
|
+
} catch {
|
|
39170
|
+
return false;
|
|
39171
|
+
}
|
|
39172
|
+
}
|
|
39173
|
+
async requestSignature(req, timeoutMs = 10 * 60 * 1e3) {
|
|
39174
|
+
const enqueueRes = await fetch(`${this.baseUrl}/requests`, {
|
|
39175
|
+
method: "POST",
|
|
39176
|
+
headers: { "Content-Type": "application/json", "x-sailor-secret": this.requestSecret },
|
|
39177
|
+
body: JSON.stringify(req)
|
|
39178
|
+
});
|
|
39179
|
+
if (!enqueueRes.ok) {
|
|
39180
|
+
throw new Error(`Failed to enqueue signing request (HTTP ${enqueueRes.status})`);
|
|
39181
|
+
}
|
|
39182
|
+
const { id } = await enqueueRes.json();
|
|
39183
|
+
const deadline = Date.now() + timeoutMs;
|
|
39184
|
+
while (Date.now() < deadline) {
|
|
39185
|
+
const res = await fetch(`${this.baseUrl}/requests/${encodeURIComponent(id)}/result`, {
|
|
39186
|
+
headers: { "x-sailor-secret": this.requestSecret }
|
|
39162
39187
|
});
|
|
39163
|
-
|
|
39164
|
-
|
|
39165
|
-
|
|
39166
|
-
console.log(
|
|
39167
|
-
`
|
|
39168
|
-
Multi-chain addresses (salt ${saltNonce}, owner ${ownerAddr2}, manager ${managerAddr2}):`
|
|
39169
|
-
);
|
|
39170
|
-
const CHAIN_NAMES = { 8453: "Base", 42161: "Arbitrum", 130: "Unichain" };
|
|
39171
|
-
for (const cid of MAINNET_CHAINS) {
|
|
39172
|
-
const dep = sailDeployments[cid];
|
|
39173
|
-
const initializer = buildSafeSetupInitializer({
|
|
39174
|
-
owners: [ownerAddr2],
|
|
39175
|
-
threshold: 1n,
|
|
39176
|
-
kernel: dep.kernel,
|
|
39177
|
-
safeModuleEnabler: dep.safeModuleEnabler
|
|
39178
|
-
});
|
|
39179
|
-
const predicted = computeSailSmaAddress({
|
|
39180
|
-
initializer,
|
|
39181
|
-
saltNonce,
|
|
39182
|
-
deployer: ownerAddr2,
|
|
39183
|
-
permissionSigner: ownerAddr2,
|
|
39184
|
-
manager: managerAddr2,
|
|
39185
|
-
feePolicy: dep.standardFeePolicy,
|
|
39186
|
-
proxyCreationCode
|
|
39187
|
-
});
|
|
39188
|
-
const isDeployed = predicted.toLowerCase() === safe.toLowerCase() && cid === chainId;
|
|
39189
|
-
const label = isDeployed ? "deployed (this account)" : predicted;
|
|
39190
|
-
console.log(` ${CHAIN_NAMES[cid].padEnd(12)} (${cid}): ${label}`);
|
|
39191
|
-
}
|
|
39192
|
-
console.log(
|
|
39193
|
-
' \u26A0 Addresses differ per chain (chain-specific kernel salt + initializer). Run "sailor account predict" for details.'
|
|
39194
|
-
);
|
|
39188
|
+
if (res.status === 200) return await res.json();
|
|
39189
|
+
if (res.status !== 204) {
|
|
39190
|
+
throw new Error(`Unexpected result status ${res.status} from signing station`);
|
|
39195
39191
|
}
|
|
39196
|
-
} catch {
|
|
39197
39192
|
}
|
|
39198
|
-
|
|
39199
|
-
console.log(
|
|
39200
|
-
"\nMulti-chain addresses: saltNonce not stored (deployed before salt tracking)."
|
|
39201
|
-
);
|
|
39202
|
-
console.log(" To enable: re-deploy with sailor onboard --new-sma --salt 0");
|
|
39203
|
-
}
|
|
39204
|
-
if (permissions.length === 0) {
|
|
39205
|
-
console.log(
|
|
39206
|
-
"\n\u26A0 No permissions registered \u2014 every dispatch will be denied (NoPermissionsRegistered)."
|
|
39207
|
-
);
|
|
39208
|
-
console.log(' Register at least one with "sailor mandate attach".');
|
|
39209
|
-
return;
|
|
39210
|
-
}
|
|
39211
|
-
console.log(`
|
|
39212
|
-
Registered permissions (${permissions.length}):`);
|
|
39213
|
-
if (permsNoCode.length > 0) {
|
|
39214
|
-
console.log(
|
|
39215
|
-
`
|
|
39216
|
-
\u26A0 ${permsNoCode.length} registered permission(s) have NO contract code on chain ${chainId} \u2014 dispatches naming them will fail. Verify the address (wrong chain?) or revoke:`
|
|
39217
|
-
);
|
|
39218
|
-
permsNoCode.forEach((p) => console.log(` ${p}`));
|
|
39219
|
-
}
|
|
39220
|
-
if (caps.dispatchModel === "selective") {
|
|
39221
|
-
permissions.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
|
|
39222
|
-
console.log("\nEach dispatch names one permission, so pass-through is not required.");
|
|
39223
|
-
return;
|
|
39193
|
+
throw new Error(`Signing request "${req.title}" timed out after ${timeoutMs / 1e3}s`);
|
|
39224
39194
|
}
|
|
39225
|
-
|
|
39226
|
-
const
|
|
39227
|
-
|
|
39228
|
-
|
|
39195
|
+
async waitForWallet(timeoutMs = 5 * 60 * 1e3) {
|
|
39196
|
+
const deadline = Date.now() + timeoutMs;
|
|
39197
|
+
while (Date.now() < deadline) {
|
|
39198
|
+
try {
|
|
39199
|
+
const r = await fetch(`${this.baseUrl}/wallet`, {
|
|
39200
|
+
headers: { "x-sailor-secret": this.requestSecret },
|
|
39201
|
+
signal: AbortSignal.timeout(2e3)
|
|
39202
|
+
});
|
|
39203
|
+
if (r.ok) {
|
|
39204
|
+
const { address } = await r.json();
|
|
39205
|
+
if (address) return address;
|
|
39206
|
+
}
|
|
39207
|
+
} catch {
|
|
39208
|
+
}
|
|
39209
|
+
await sleep(1e3);
|
|
39210
|
+
}
|
|
39211
|
+
throw new Error("Timed out waiting for wallet connection in the signing UI");
|
|
39229
39212
|
}
|
|
39230
|
-
|
|
39231
|
-
|
|
39232
|
-
|
|
39233
|
-
|
|
39234
|
-
|
|
39235
|
-
|
|
39236
|
-
}
|
|
39237
|
-
|
|
39213
|
+
};
|
|
39214
|
+
function readRuntimeServerState(projectRoot) {
|
|
39215
|
+
const file = (0, import_node_path5.join)(projectRoot, RUNTIME_SERVER_FILE);
|
|
39216
|
+
if (!(0, import_node_fs6.existsSync)(file)) return null;
|
|
39217
|
+
try {
|
|
39218
|
+
return JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8"));
|
|
39219
|
+
} catch {
|
|
39220
|
+
return null;
|
|
39238
39221
|
}
|
|
39239
|
-
console.log(`
|
|
39240
|
-
Probe is heuristic: an unknown selector (${PROBE_SELECTOR}) to a neutral target (${PROBE_TARGET}).`);
|
|
39241
39222
|
}
|
|
39242
|
-
|
|
39243
|
-
|
|
39244
|
-
|
|
39245
|
-
|
|
39246
|
-
|
|
39247
|
-
// src/lib/foundry.ts
|
|
39248
|
-
var import_node_fs4 = require("node:fs");
|
|
39249
|
-
var import_node_path3 = require("node:path");
|
|
39250
|
-
var FOUNDRY_TOML = `[profile.default]
|
|
39251
|
-
src = "mandates"
|
|
39252
|
-
out = "out"
|
|
39253
|
-
libs = ["lib"]
|
|
39254
|
-
remappings = ["@sail/=.sail/contracts/"]
|
|
39255
|
-
solc = "0.8.26"
|
|
39256
|
-
optimizer = true
|
|
39257
|
-
optimizer_runs = 200
|
|
39258
|
-
# Mandates are deployed as standalone contracts and configured via their
|
|
39259
|
-
# constructor, then attached to a Safe with \`sailor mandate attach\`.
|
|
39260
|
-
`;
|
|
39261
|
-
var IPERMISSION_SOL = `// SPDX-License-Identifier: MIT
|
|
39262
|
-
pragma solidity 0.8.26;
|
|
39263
|
-
|
|
39264
|
-
/// @notice Execution context passed to every permission on each dispatch call.
|
|
39265
|
-
/// @dev Read-only snapshot of the transaction environment (staticcall).
|
|
39266
|
-
struct Context {
|
|
39267
|
-
address account; // the Safe whose assets are being moved
|
|
39268
|
-
address manager; // the delegated signer who submitted the dispatch
|
|
39269
|
-
address submitter; // msg.sender of the dispatch (may be a relayer)
|
|
39270
|
-
address target; // the call target
|
|
39271
|
-
bytes4 selector; // leading 4 bytes of calldata
|
|
39272
|
-
uint256 value; // native ETH forwarded (wei)
|
|
39273
|
-
uint256 blockTimestamp; // block.timestamp at dispatch
|
|
39274
|
-
uint256 blockNumber; // block.number at dispatch
|
|
39223
|
+
async function discoverDaemon(projectRoot = process.cwd()) {
|
|
39224
|
+
const state = readRuntimeServerState(projectRoot);
|
|
39225
|
+
if (!state?.url) return null;
|
|
39226
|
+
const client = new SigningClient(state.url, state.requestSecret ?? "");
|
|
39227
|
+
return await client.ping() ? client : null;
|
|
39275
39228
|
}
|
|
39276
|
-
|
|
39277
|
-
|
|
39278
|
-
|
|
39279
|
-
|
|
39280
|
-
/// exhaustion is treated as \`false\`. Must not mutate state.
|
|
39281
|
-
interface IPermission {
|
|
39282
|
-
/// @notice Decide whether a manager-submitted transaction is permitted.
|
|
39283
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
|
|
39284
|
-
|
|
39285
|
-
/// @notice Optional stable identifier for off-chain indexing/deduplication.
|
|
39286
|
-
function discriminator() external view returns (bytes32);
|
|
39229
|
+
async function createSigningChannel(projectRoot = process.cwd()) {
|
|
39230
|
+
const daemon = await discoverDaemon(projectRoot);
|
|
39231
|
+
if (daemon) return daemon;
|
|
39232
|
+
return new SigningServer({ projectRoot, advertise: false });
|
|
39287
39233
|
}
|
|
39288
|
-
`;
|
|
39289
|
-
var EXAMPLE_MANDATE_SOL = `// SPDX-License-Identifier: MIT
|
|
39290
|
-
pragma solidity 0.8.26;
|
|
39291
|
-
|
|
39292
|
-
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
39293
|
-
|
|
39294
|
-
/// @title BoundedCallPermission
|
|
39295
|
-
/// @notice General-purpose IPermission primitive. Bounds the universal properties of any call:
|
|
39296
|
-
/// allowed targets, allowed selectors, and max ETH value. Protocol-agnostic.
|
|
39297
|
-
/// For calldata-parameter bounds (amount caps, recipient checks, slippage), write a
|
|
39298
|
-
/// protocol-specific permission \u2014 see examples/permissions/ for the pattern per protocol.
|
|
39299
|
-
/// @dev Deploy one instance per SMA with constructor-configured parameters.
|
|
39300
|
-
contract BoundedCallPermission is IPermission {
|
|
39301
|
-
bytes32 private constant DISCRIMINATOR = keccak256("BoundedCallPermission");
|
|
39302
|
-
|
|
39303
|
-
mapping(address => bool) public isAllowedTarget;
|
|
39304
|
-
mapping(bytes4 => bool) public isAllowedSelector;
|
|
39305
|
-
bool public immutable SELECTOR_FILTERING;
|
|
39306
|
-
uint256 public immutable MAX_VALUE;
|
|
39307
|
-
|
|
39308
|
-
constructor(address[] memory allowedTargets, bytes4[] memory allowedSelectors, uint256 maxValue) {
|
|
39309
|
-
for (uint256 i = 0; i < allowedTargets.length; i++) isAllowedTarget[allowedTargets[i]] = true;
|
|
39310
|
-
SELECTOR_FILTERING = allowedSelectors.length > 0;
|
|
39311
|
-
for (uint256 i = 0; i < allowedSelectors.length; i++) isAllowedSelector[allowedSelectors[i]] = true;
|
|
39312
|
-
MAX_VALUE = maxValue;
|
|
39313
|
-
}
|
|
39314
|
-
|
|
39315
|
-
function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) {
|
|
39316
|
-
if (!isAllowedTarget[ctx.target]) return false;
|
|
39317
|
-
if (SELECTOR_FILTERING && !isAllowedSelector[ctx.selector]) return false;
|
|
39318
|
-
if (ctx.value > MAX_VALUE) return false;
|
|
39319
|
-
return true;
|
|
39320
|
-
}
|
|
39321
39234
|
|
|
39322
|
-
|
|
39235
|
+
// src/commands/account.ts
|
|
39236
|
+
function resolveChain(chainId) {
|
|
39237
|
+
try {
|
|
39238
|
+
return getChain(chainId);
|
|
39239
|
+
} catch {
|
|
39240
|
+
throw new Error(
|
|
39241
|
+
`Chain ${chainId} is not yet configured in @sail/chains.
|
|
39242
|
+
The SailKernel and mandate-factory addresses for this chain are unknown,
|
|
39243
|
+
so an account cannot be created yet. Add the chain to @sail/chains once
|
|
39244
|
+
SailKernel is deployed there.`
|
|
39245
|
+
);
|
|
39246
|
+
}
|
|
39323
39247
|
}
|
|
39324
|
-
|
|
39325
|
-
|
|
39326
|
-
|
|
39327
|
-
|
|
39328
|
-
|
|
39329
|
-
|
|
39330
|
-
|
|
39331
|
-
|
|
39332
|
-
|
|
39333
|
-
|
|
39334
|
-
|
|
39335
|
-
|
|
39336
|
-
|
|
39337
|
-
|
|
39338
|
-
|
|
39339
|
-
|
|
39340
|
-
|
|
39341
|
-
|
|
39342
|
-
|
|
39343
|
-
|
|
39344
|
-
|
|
39345
|
-
|
|
39346
|
-
|
|
39347
|
-
|
|
39348
|
-
|
|
39349
|
-
|
|
39350
|
-
|
|
39351
|
-
|
|
39352
|
-
|
|
39353
|
-
|
|
39354
|
-
|
|
39355
|
-
|
|
39356
|
-
|
|
39357
|
-
|
|
39358
|
-
|
|
39359
|
-
|
|
39360
|
-
|
|
39361
|
-
const
|
|
39362
|
-
|
|
39363
|
-
|
|
39248
|
+
async function accountCreate() {
|
|
39249
|
+
if (!keyExists("manager")) {
|
|
39250
|
+
throw new Error(
|
|
39251
|
+
'No agent wallet found.\nRun "sailor keys generate" and choose "agent wallet" first.'
|
|
39252
|
+
);
|
|
39253
|
+
}
|
|
39254
|
+
const env = parseEnvFile(sailPath(".env.local"));
|
|
39255
|
+
const rpcUrl = env["RPC_URL"] ?? process.env["RPC_URL"];
|
|
39256
|
+
const chainIdRaw = env["CHAIN_ID"] ?? process.env["CHAIN_ID"];
|
|
39257
|
+
if (!rpcUrl || !chainIdRaw) {
|
|
39258
|
+
throw new Error(
|
|
39259
|
+
"RPC_URL and CHAIN_ID must be set in .sail/.env.local.\nCreate that file with, for example:\n RPC_URL=https://your-rpc-endpoint\n CHAIN_ID=8453"
|
|
39260
|
+
);
|
|
39261
|
+
}
|
|
39262
|
+
const chainId = Number(chainIdRaw);
|
|
39263
|
+
if (Number.isNaN(chainId)) {
|
|
39264
|
+
throw new Error(`Invalid CHAIN_ID: "${chainIdRaw}" \u2014 must be a number.`);
|
|
39265
|
+
}
|
|
39266
|
+
const chain2 = resolveChain(chainId);
|
|
39267
|
+
console.log(`Chain ${chainId} (${chain2.name})`);
|
|
39268
|
+
console.log(` SailKernel: ${checksum4(chain2.kernel)}`);
|
|
39269
|
+
console.log(` Mandate factory: ${checksum4(chain2.mandateFactory)}
|
|
39270
|
+
`);
|
|
39271
|
+
const manager = await loadKeyring("manager");
|
|
39272
|
+
const managerAddr = checksum4(manager.address);
|
|
39273
|
+
const safeFactory = await promptAddress("Safe factory address");
|
|
39274
|
+
const safeSingleton = await promptAddress("Safe singleton address");
|
|
39275
|
+
const owner2 = await promptAddress("Owner (EOA) address", managerAddr);
|
|
39276
|
+
const permissionSigner = await promptAddress("Mandate signer address", managerAddr);
|
|
39277
|
+
const feePolicy = await prompt("Fee policy", "none");
|
|
39278
|
+
console.log("\nCreating SMA with:");
|
|
39279
|
+
console.log(` Owner: ${owner2}`);
|
|
39280
|
+
console.log(` Agent wallet: ${managerAddr}`);
|
|
39281
|
+
console.log(` Mandate signer: ${permissionSigner}`);
|
|
39282
|
+
console.log(` Safe factory: ${safeFactory}`);
|
|
39283
|
+
console.log(` Safe singleton: ${safeSingleton}`);
|
|
39284
|
+
console.log(` Fee policy: ${feePolicy}`);
|
|
39285
|
+
const client = makeClient(chainId);
|
|
39286
|
+
try {
|
|
39287
|
+
const account2 = await client.account.create({
|
|
39288
|
+
owner: owner2,
|
|
39289
|
+
permissionSigner,
|
|
39290
|
+
manager: managerAddr,
|
|
39291
|
+
chainId
|
|
39292
|
+
});
|
|
39293
|
+
const stored = {
|
|
39294
|
+
safe: checksum4(account2.safe),
|
|
39295
|
+
owner: checksum4(account2.owner),
|
|
39296
|
+
permissionSigner: checksum4(account2.permissionSigner),
|
|
39297
|
+
manager: checksum4(account2.manager),
|
|
39298
|
+
chainId: account2.chainId,
|
|
39299
|
+
createdAtBlock: account2.createdAtBlock.toString()
|
|
39300
|
+
};
|
|
39301
|
+
upsertAccountInList(stored);
|
|
39302
|
+
writeJsonFile(sailPath("account.json"), stored);
|
|
39303
|
+
console.log(`
|
|
39304
|
+
SMA created. Address: ${stored.safe}`);
|
|
39305
|
+
console.log("Saved to .sail/account.json");
|
|
39306
|
+
} catch (err) {
|
|
39307
|
+
if (err.message === "not implemented") {
|
|
39308
|
+
console.log(
|
|
39309
|
+
"\nOn-chain account creation is not wired up in this build yet \u2014\nclient.account.create is a stub until SailKernel is deployed and the\nSDK is connected. Nothing was created on-chain."
|
|
39310
|
+
);
|
|
39311
|
+
return;
|
|
39312
|
+
}
|
|
39313
|
+
throw err;
|
|
39364
39314
|
}
|
|
39365
|
-
writeIfMissing((0, import_node_path3.join)(root, "foundry.toml"), FOUNDRY_TOML);
|
|
39366
|
-
writeIfMissing(
|
|
39367
|
-
(0, import_node_path3.join)(root, ".sail", "contracts", "interfaces", "IPermission.sol"),
|
|
39368
|
-
IPERMISSION_SOL
|
|
39369
|
-
);
|
|
39370
|
-
writeIfMissing((0, import_node_path3.join)(root, "mandates", "BoundedCallPermission.sol"), EXAMPLE_MANDATE_SOL);
|
|
39371
|
-
writeIfMissing((0, import_node_path3.join)(root, "mandates", "README.md"), MANDATES_README);
|
|
39372
39315
|
}
|
|
39373
|
-
|
|
39374
|
-
|
|
39316
|
+
var SAIL_MAINNET_CHAINS = [1, 8453, 42161, 130];
|
|
39317
|
+
async function fetchProxyCreationCode(preferredChainId) {
|
|
39318
|
+
const rpcUrl = getRpcUrl(preferredChainId) ?? void 0;
|
|
39319
|
+
const publicClient = createPublicClient({
|
|
39320
|
+
chain: getChainById(preferredChainId),
|
|
39321
|
+
transport: http(rpcUrl)
|
|
39322
|
+
});
|
|
39323
|
+
return await publicClient.readContract({
|
|
39324
|
+
address: SAFE_V141.proxyFactory,
|
|
39325
|
+
abi: safeProxyFactoryAbi,
|
|
39326
|
+
functionName: "proxyCreationCode"
|
|
39327
|
+
});
|
|
39375
39328
|
}
|
|
39376
|
-
|
|
39377
|
-
|
|
39378
|
-
|
|
39379
|
-
|
|
39380
|
-
|
|
39381
|
-
|
|
39382
|
-
|
|
39383
|
-
|
|
39384
|
-
|
|
39329
|
+
async function accountPredict(options) {
|
|
39330
|
+
const stored = readJsonFile(sailPath("account.json"));
|
|
39331
|
+
let ownerAddr;
|
|
39332
|
+
if (options.owner) {
|
|
39333
|
+
if (!isAddress(options.owner, { strict: false })) {
|
|
39334
|
+
throw new Error(`Invalid --owner address: ${options.owner}`);
|
|
39335
|
+
}
|
|
39336
|
+
ownerAddr = getAddress(options.owner);
|
|
39337
|
+
} else {
|
|
39338
|
+
if (!stored?.owner) {
|
|
39339
|
+
throw new Error(
|
|
39340
|
+
"No owner found in .sail/account.json. Pass --owner <address> or run sailor onboard first."
|
|
39341
|
+
);
|
|
39342
|
+
}
|
|
39343
|
+
ownerAddr = getAddress(stored.owner);
|
|
39385
39344
|
}
|
|
39386
|
-
|
|
39387
|
-
|
|
39388
|
-
|
|
39389
|
-
|
|
39390
|
-
for (let depth = 0; depth < 6; depth++) {
|
|
39391
|
-
const pkgFile = import_node_path4.default.join(dir, "package.json");
|
|
39392
|
-
if (import_node_fs5.default.existsSync(pkgFile)) {
|
|
39393
|
-
try {
|
|
39394
|
-
const pkg = JSON.parse(import_node_fs5.default.readFileSync(pkgFile, "utf-8"));
|
|
39395
|
-
if (pkg.bin?.sailor) {
|
|
39396
|
-
if (firstBinMatch === null) firstBinMatch = dir;
|
|
39397
|
-
if (import_node_fs5.default.existsSync(import_node_path4.default.join(dir, "templates"))) return dir;
|
|
39398
|
-
}
|
|
39399
|
-
} catch {
|
|
39400
|
-
}
|
|
39345
|
+
let managerAddr;
|
|
39346
|
+
if (options.manager) {
|
|
39347
|
+
if (!isAddress(options.manager, { strict: false })) {
|
|
39348
|
+
throw new Error(`Invalid --manager address: ${options.manager}`);
|
|
39401
39349
|
}
|
|
39402
|
-
|
|
39403
|
-
|
|
39404
|
-
|
|
39350
|
+
managerAddr = getAddress(options.manager);
|
|
39351
|
+
} else if (stored?.manager) {
|
|
39352
|
+
managerAddr = getAddress(stored.manager);
|
|
39353
|
+
} else {
|
|
39354
|
+
throw new Error(
|
|
39355
|
+
"The predicted address depends on the agent (manager) wallet, which is mixed into the kernel's CREATE2 salt.\nPass --manager <agent address> (create one first with `sailor keys`), or run after onboarding so it can be read from .sail/account.json."
|
|
39356
|
+
);
|
|
39405
39357
|
}
|
|
39406
|
-
|
|
39407
|
-
}
|
|
39408
|
-
|
|
39409
|
-
const
|
|
39410
|
-
|
|
39411
|
-
|
|
39412
|
-
|
|
39413
|
-
|
|
39414
|
-
|
|
39415
|
-
|
|
39416
|
-
|
|
39417
|
-
"out",
|
|
39418
|
-
"cache",
|
|
39419
|
-
"broadcast",
|
|
39420
|
-
".git"
|
|
39421
|
-
]);
|
|
39422
|
-
function copyDirSync(src, dest) {
|
|
39423
|
-
import_node_fs6.default.mkdirSync(dest, { recursive: true });
|
|
39424
|
-
for (const entry of import_node_fs6.default.readdirSync(src, { withFileTypes: true })) {
|
|
39425
|
-
if (TEMPLATE_COPY_EXCLUDES.has(entry.name)) continue;
|
|
39426
|
-
const srcPath = import_node_path5.default.join(src, entry.name);
|
|
39427
|
-
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
39428
|
-
const destPath = import_node_path5.default.join(dest, destName);
|
|
39429
|
-
if (entry.isDirectory()) {
|
|
39430
|
-
copyDirSync(srcPath, destPath);
|
|
39431
|
-
} else {
|
|
39432
|
-
import_node_fs6.default.copyFileSync(srcPath, destPath);
|
|
39358
|
+
if (options.salt != null && !/^\d+$/.test(options.salt)) {
|
|
39359
|
+
throw new Error(`Invalid --salt value: "${options.salt}" \u2014 must be a non-negative integer.`);
|
|
39360
|
+
}
|
|
39361
|
+
const saltNonce = options.salt != null ? BigInt(options.salt) : 0n;
|
|
39362
|
+
let chainIds;
|
|
39363
|
+
if (options.chain) {
|
|
39364
|
+
const chainId = Number(options.chain);
|
|
39365
|
+
if (!(chainId in sailDeployments)) {
|
|
39366
|
+
throw new Error(
|
|
39367
|
+
`Chain ${chainId} has no Sail Protocol deployment. Supported: ${Object.keys(sailDeployments).join(", ")}`
|
|
39368
|
+
);
|
|
39433
39369
|
}
|
|
39370
|
+
chainIds = [chainId];
|
|
39371
|
+
} else {
|
|
39372
|
+
chainIds = SAIL_MAINNET_CHAINS;
|
|
39434
39373
|
}
|
|
39374
|
+
const preferredChain = chainIds.find((cid) => getRpcUrl(cid) != null) ?? chainIds[0];
|
|
39375
|
+
const proxyCreationCode = await fetchProxyCreationCode(preferredChain);
|
|
39376
|
+
const results = chainIds.map((chainId) => {
|
|
39377
|
+
const deployment = sailDeployments[chainId];
|
|
39378
|
+
const viemChain = getChainById(chainId);
|
|
39379
|
+
const { predicted: predictedAddress } = buildSmaAddressPrediction(
|
|
39380
|
+
deployment,
|
|
39381
|
+
ownerAddr,
|
|
39382
|
+
managerAddr,
|
|
39383
|
+
saltNonce,
|
|
39384
|
+
proxyCreationCode
|
|
39385
|
+
);
|
|
39386
|
+
return {
|
|
39387
|
+
chainId,
|
|
39388
|
+
name: viemChain.name,
|
|
39389
|
+
predictedAddress,
|
|
39390
|
+
kernel: deployment.kernel,
|
|
39391
|
+
safeModuleEnabler: deployment.safeModuleEnabler
|
|
39392
|
+
};
|
|
39393
|
+
});
|
|
39394
|
+
const uniqueAddresses = new Set(results.map((r) => r.predictedAddress.toLowerCase()));
|
|
39395
|
+
const allSame = uniqueAddresses.size === 1;
|
|
39396
|
+
if (options.json) {
|
|
39397
|
+
console.log(
|
|
39398
|
+
JSON.stringify(
|
|
39399
|
+
{
|
|
39400
|
+
salt: saltNonce.toString(),
|
|
39401
|
+
owner: ownerAddr,
|
|
39402
|
+
manager: managerAddr,
|
|
39403
|
+
chains: results.map(({ chainId, name, predictedAddress }) => ({
|
|
39404
|
+
chainId,
|
|
39405
|
+
name,
|
|
39406
|
+
predictedAddress
|
|
39407
|
+
})),
|
|
39408
|
+
allSame,
|
|
39409
|
+
note: allSame ? "All chains produce the same address with this salt, owner, and manager." : "Addresses differ per chain because the kernel salt binds the chain-specific fee policy and the Safe initializer encodes chain-specific contract addresses (kernel, safeModuleEnabler). Cross-chain same-address requires deterministic protocol deployment or a registerExisting() flow."
|
|
39410
|
+
},
|
|
39411
|
+
null,
|
|
39412
|
+
2
|
|
39413
|
+
)
|
|
39414
|
+
);
|
|
39415
|
+
return;
|
|
39416
|
+
}
|
|
39417
|
+
console.log("\nPredicted Safe addresses");
|
|
39418
|
+
console.log(` Owner: ${ownerAddr}`);
|
|
39419
|
+
console.log(` Manager: ${managerAddr}`);
|
|
39420
|
+
console.log(` Salt: ${saltNonce}`);
|
|
39421
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
39422
|
+
for (const { chainId, name, predictedAddress } of results) {
|
|
39423
|
+
console.log(` ${name.padEnd(14)} (${String(chainId).padEnd(5)}): ${predictedAddress}`);
|
|
39424
|
+
}
|
|
39425
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
39426
|
+
if (allSame) {
|
|
39427
|
+
console.log("\u2713 All chains produce the same address.");
|
|
39428
|
+
} else {
|
|
39429
|
+
console.log("\n\u26A0 Addresses differ across chains.");
|
|
39430
|
+
console.log(
|
|
39431
|
+
" Root cause: SailKernel.createAccount binds the CREATE2 salt as\n keccak256(saltNonce, deployer, permissionSigner, manager, feePolicy),\n then SafeProxyFactory derives the address from that bound salt and the\n Safe initializer. Both the fee policy and the initializer (kernel,\n safeModuleEnabler) are chain-specific, so each chain yields a different\n address even with the same owner, manager, and salt.\n\n For cross-chain same-address the Sail Protocol needs one of:\n A) Deterministic (CREATE2) deployment of kernel + safeModuleEnabler +\n fee policy so they land at the same address on every chain.\n B) A registerExisting() path allowing a plain Safe (deployed with a\n chain-agnostic initializer) to be registered with the kernel."
|
|
39432
|
+
);
|
|
39433
|
+
}
|
|
39434
|
+
console.log(`
|
|
39435
|
+
To deploy on this chain: sailor onboard --new-sma --salt ${saltNonce}`);
|
|
39435
39436
|
}
|
|
39436
|
-
|
|
39437
|
-
|
|
39438
|
-
|
|
39439
|
-
|
|
39440
|
-
|
|
39441
|
-
|
|
39442
|
-
|
|
39443
|
-
|
|
39444
|
-
|
|
39445
|
-
|
|
39446
|
-
|
|
39447
|
-
AI coding agents should read the project's \`AGENTS.md\` and this folder's \`config.json\`
|
|
39448
|
-
before changing strategy code or running commands that touch funds.
|
|
39449
|
-
`;
|
|
39450
|
-
function writeIfMissing2(file, content) {
|
|
39451
|
-
if (!import_node_fs6.default.existsSync(file)) import_node_fs6.default.writeFileSync(file, content, "utf-8");
|
|
39452
|
-
}
|
|
39453
|
-
function scaffoldProjectWorkspace(dest, name, options) {
|
|
39454
|
-
const chainId = options.chain ? (() => {
|
|
39455
|
-
const n = Number(options.chain);
|
|
39456
|
-
if (!Number.isInteger(n) || n <= 0) throw new Error(`Invalid chain id: "${options.chain}"`);
|
|
39457
|
-
return n;
|
|
39458
|
-
})() : null;
|
|
39459
|
-
const sailDir2 = import_node_path5.default.join(dest, ".sail");
|
|
39460
|
-
import_node_fs6.default.mkdirSync(import_node_path5.default.join(sailDir2, "keys"), { recursive: true });
|
|
39461
|
-
import_node_fs6.default.mkdirSync(import_node_path5.default.join(sailDir2, "runtime"), { recursive: true });
|
|
39462
|
-
import_node_fs6.default.mkdirSync(import_node_path5.default.join(sailDir2, "state"), { recursive: true });
|
|
39463
|
-
import_node_fs6.default.writeFileSync(
|
|
39464
|
-
import_node_path5.default.join(sailDir2, "config.json"),
|
|
39465
|
-
`${JSON.stringify(
|
|
39466
|
-
{
|
|
39467
|
-
version: 1,
|
|
39468
|
-
name,
|
|
39469
|
-
chainId,
|
|
39470
|
-
// null = chain not yet chosen; Stage 1 will set this
|
|
39471
|
-
stateDir: ".sail/state",
|
|
39472
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39473
|
-
contracts: {
|
|
39474
|
-
kernel: "",
|
|
39475
|
-
mandateFactory: ""
|
|
39476
|
-
}
|
|
39477
|
-
},
|
|
39478
|
-
null,
|
|
39479
|
-
2
|
|
39480
|
-
)}
|
|
39481
|
-
`,
|
|
39482
|
-
"utf-8"
|
|
39483
|
-
);
|
|
39484
|
-
writeIfMissing2(import_node_path5.default.join(sailDir2, "README.md"), SAIL_WORKSPACE_README);
|
|
39485
|
-
const chainIdLine = chainId != null ? `CHAIN_ID=${chainId}
|
|
39486
|
-
` : `# CHAIN_ID=8453 # set after choosing your chain in Stage 1
|
|
39487
|
-
`;
|
|
39488
|
-
import_node_fs6.default.writeFileSync(
|
|
39489
|
-
import_node_path5.default.join(dest, ".env.example"),
|
|
39490
|
-
`# Sailor agent environment
|
|
39491
|
-
RPC_URL=https://your-rpc-endpoint
|
|
39492
|
-
${chainIdLine}
|
|
39493
|
-
# Optional for non-interactive runs
|
|
39494
|
-
# SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
|
|
39495
|
-
`,
|
|
39496
|
-
"utf-8"
|
|
39497
|
-
);
|
|
39498
|
-
const rpcLine = options.rpcUrl ? `RPC_URL=${options.rpcUrl}` : `# Paste your RPC endpoint here (Alchemy, Infura, or any HTTPS endpoint)
|
|
39499
|
-
# RPC_URL=https://your-rpc-endpoint`;
|
|
39500
|
-
const chainLine = chainId != null ? `
|
|
39501
|
-
CHAIN_ID=${chainId}` : ``;
|
|
39502
|
-
writeIfMissing2(
|
|
39503
|
-
import_node_path5.default.join(sailDir2, ".env.local"),
|
|
39504
|
-
`${rpcLine}${chainLine}
|
|
39505
|
-
|
|
39506
|
-
# Optional for non-interactive runs (CI, GitHub Actions, launchd, systemd)
|
|
39507
|
-
# SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
|
|
39508
|
-
`
|
|
39509
|
-
);
|
|
39510
|
-
}
|
|
39511
|
-
async function initCommand(dir, options = {}) {
|
|
39512
|
-
const inPlace = !dir || dir === ".";
|
|
39513
|
-
const dest = inPlace ? process.cwd() : import_node_path5.default.resolve(process.cwd(), dir);
|
|
39514
|
-
const name = import_node_path5.default.basename(dest);
|
|
39515
|
-
const templatesDir = import_node_path5.default.join(packageRoot(), "templates");
|
|
39516
|
-
const templateName = options.template ?? "default";
|
|
39517
|
-
if (/[/\\.]/.test(templateName) || templateName.includes("..")) {
|
|
39518
|
-
throw new Error(`Invalid template name: "${templateName}"`);
|
|
39437
|
+
async function accountDeployChain(options) {
|
|
39438
|
+
const json = !!options.json;
|
|
39439
|
+
const say = (fn) => {
|
|
39440
|
+
if (!json) fn();
|
|
39441
|
+
};
|
|
39442
|
+
const stored = readJsonFile(sailPath("account.json"));
|
|
39443
|
+
if (!stored?.safe || !stored?.owner || !stored?.manager) {
|
|
39444
|
+
throw new Error(
|
|
39445
|
+
"No SMA found in .sail/account.json. Run `sailor onboard --new-sma` first."
|
|
39446
|
+
);
|
|
39519
39447
|
}
|
|
39520
|
-
|
|
39521
|
-
|
|
39522
|
-
|
|
39523
|
-
|
|
39524
|
-
const hint = available === "none" ? `
|
|
39525
|
-
No templates found under ${templatesDir}.
|
|
39526
|
-
If you're running the in-tree CLI bundle from a monorepo checkout, the scaffolder
|
|
39527
|
-
couldn't locate the repo's templates/ directory. Install the published package, or
|
|
39528
|
-
run from the repo root.` : ` Available: ${available}`;
|
|
39529
|
-
throw new Error(`Template "${templateName}" not found.${hint}`);
|
|
39448
|
+
if (stored.saltNonce == null && options.salt == null) {
|
|
39449
|
+
throw new Error(
|
|
39450
|
+
"No saltNonce stored in .sail/account.json.\nPass --salt <n> explicitly, or re-deploy your SMA with `sailor onboard --new-sma --salt <n>`\nso the salt is recorded for cross-chain use."
|
|
39451
|
+
);
|
|
39530
39452
|
}
|
|
39531
|
-
|
|
39532
|
-
|
|
39533
|
-
throw new Error(`Directory must be inside the current working directory`);
|
|
39453
|
+
if (options.salt != null && !/^\d+$/.test(options.salt)) {
|
|
39454
|
+
throw new Error(`Invalid --salt value: "${options.salt}" \u2014 must be a non-negative integer.`);
|
|
39534
39455
|
}
|
|
39535
|
-
|
|
39536
|
-
|
|
39456
|
+
const ownerAddr = getAddress(stored.owner);
|
|
39457
|
+
const managerAddr = getAddress(stored.manager);
|
|
39458
|
+
const storedSafe = getAddress(stored.safe);
|
|
39459
|
+
const saltNonce = options.salt != null ? BigInt(options.salt) : BigInt(stored.saltNonce);
|
|
39460
|
+
if (!/^\d+$/.test(options.chain)) {
|
|
39461
|
+
throw new Error(`Invalid --chain value: "${options.chain}" \u2014 must be a numeric chain ID.`);
|
|
39537
39462
|
}
|
|
39538
|
-
|
|
39539
|
-
|
|
39463
|
+
const targetChainId = Number(options.chain);
|
|
39464
|
+
if (!(targetChainId in sailDeployments)) {
|
|
39465
|
+
throw new Error(
|
|
39466
|
+
`Chain ${targetChainId} has no Sail Protocol deployment.
|
|
39467
|
+
Supported: ${Object.keys(sailDeployments).join(", ")}`
|
|
39468
|
+
);
|
|
39469
|
+
}
|
|
39470
|
+
if (targetChainId === stored.chainId) {
|
|
39471
|
+
throw new Error(
|
|
39472
|
+
`Chain ${targetChainId} is already the primary chain for this SMA.
|
|
39473
|
+
Use a different chain ID.`
|
|
39474
|
+
);
|
|
39475
|
+
}
|
|
39476
|
+
const allChainIds = Object.keys(sailDeployments).map(Number);
|
|
39477
|
+
const rpcPreferred = allChainIds.find((cid) => getRpcUrl(cid) != null);
|
|
39478
|
+
if (rpcPreferred == null) {
|
|
39479
|
+
throw new Error(
|
|
39480
|
+
"No RPC URL configured for any supported chain.\nSet RPC_URL or RPC_URL_<CHAIN_ID> in .sail/.env.local."
|
|
39481
|
+
);
|
|
39482
|
+
}
|
|
39483
|
+
const proxyCreationCode = await fetchProxyCreationCode(rpcPreferred);
|
|
39484
|
+
const deployment = sailDeployments[targetChainId];
|
|
39485
|
+
const { initializer, predicted } = buildSmaAddressPrediction(
|
|
39486
|
+
deployment,
|
|
39487
|
+
ownerAddr,
|
|
39488
|
+
managerAddr,
|
|
39489
|
+
saltNonce,
|
|
39490
|
+
proxyCreationCode
|
|
39491
|
+
);
|
|
39492
|
+
if (predicted.toLowerCase() !== storedSafe.toLowerCase()) {
|
|
39493
|
+
const msg = `Your existing SMA (${storedSafe}) cannot be reproduced at the same address on
|
|
39494
|
+
chain ${targetChainId}. Predicted address: ${predicted}.
|
|
39495
|
+
|
|
39496
|
+
Two possible causes:
|
|
39497
|
+
a) Wrong --salt value. The stored deployment salt is ${stored.saltNonce ?? "unknown"}. Re-run without --salt to use it automatically.
|
|
39498
|
+
b) SMA was deployed against the old per-chain contracts (pre-deterministic
|
|
39499
|
+
kernel deployment). The current contracts are identical across all chains.
|
|
39500
|
+
|
|
39501
|
+
If it is (b), deploy a new SMA with the current contracts:
|
|
39502
|
+
sailor onboard --new-sma --salt ${stored.saltNonce ?? saltNonce}
|
|
39503
|
+
Then run deploy-chain from that account.`;
|
|
39504
|
+
if (json) {
|
|
39505
|
+
console.log(
|
|
39506
|
+
JSON.stringify(
|
|
39507
|
+
{
|
|
39508
|
+
status: "error",
|
|
39509
|
+
error: "old-contracts",
|
|
39510
|
+
stored: storedSafe,
|
|
39511
|
+
predicted,
|
|
39512
|
+
targetChainId,
|
|
39513
|
+
message: msg
|
|
39514
|
+
},
|
|
39515
|
+
null,
|
|
39516
|
+
2
|
|
39517
|
+
)
|
|
39518
|
+
);
|
|
39519
|
+
process.exit(1);
|
|
39520
|
+
}
|
|
39521
|
+
throw new Error(msg);
|
|
39540
39522
|
}
|
|
39541
|
-
|
|
39542
|
-
|
|
39543
|
-
|
|
39544
|
-
|
|
39545
|
-
|
|
39546
|
-
|
|
39547
|
-
|
|
39548
|
-
|
|
39549
|
-
|
|
39550
|
-
|
|
39551
|
-
|
|
39552
|
-
|
|
39553
|
-
|
|
39554
|
-
|
|
39555
|
-
|
|
39556
|
-
|
|
39557
|
-
if (deps["@sail/sdk"] === "workspace:*") {
|
|
39558
|
-
const sdkPath = import_node_path5.default.join(pkgRoot, "packages", "sdk");
|
|
39559
|
-
deps["@sail/sdk"] = import_node_fs6.default.existsSync(sdkPath) ? `file:${sdkPath}` : (
|
|
39560
|
-
// Fallback: SDK not bundled — user must install it manually.
|
|
39561
|
-
"0.1.0"
|
|
39523
|
+
const targetClient = createPublicClient({
|
|
39524
|
+
chain: getChainById(targetChainId),
|
|
39525
|
+
transport: http(getRpcUrl(targetChainId) ?? void 0)
|
|
39526
|
+
});
|
|
39527
|
+
const alreadyRecorded = (stored.deployedChains ?? []).includes(targetChainId);
|
|
39528
|
+
if (alreadyRecorded) {
|
|
39529
|
+
say(() => console.log(`
|
|
39530
|
+
Chain ${targetChainId} is already recorded as deployed \u2014 verifying on-chain\u2026`));
|
|
39531
|
+
}
|
|
39532
|
+
const existingCode = await targetClient.getCode({ address: predicted });
|
|
39533
|
+
if (existingCode && existingCode !== "0x") {
|
|
39534
|
+
say(() => console.log(`SMA confirmed at ${predicted} on chain ${targetChainId}.`));
|
|
39535
|
+
if (!alreadyRecorded) recordDeployedChain(stored, targetChainId);
|
|
39536
|
+
if (json) {
|
|
39537
|
+
console.log(
|
|
39538
|
+
JSON.stringify({ status: "ok", alreadyDeployed: true, address: predicted, chainId: targetChainId }, null, 2)
|
|
39562
39539
|
);
|
|
39563
39540
|
}
|
|
39564
|
-
|
|
39565
|
-
import_node_fs6.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
39566
|
-
`);
|
|
39541
|
+
return;
|
|
39567
39542
|
}
|
|
39568
|
-
|
|
39569
|
-
|
|
39570
|
-
|
|
39571
|
-
|
|
39572
|
-
|
|
39573
|
-
inPlace,
|
|
39574
|
-
!!options.rpcUrl,
|
|
39575
|
-
/* freshInit */
|
|
39576
|
-
true
|
|
39543
|
+
say(
|
|
39544
|
+
() => console.log(
|
|
39545
|
+
`
|
|
39546
|
+
Deploying SMA on ${getChainById(targetChainId).name} (chain ${targetChainId})\u2026`
|
|
39547
|
+
)
|
|
39577
39548
|
);
|
|
39578
|
-
}
|
|
39579
|
-
|
|
39580
|
-
|
|
39581
|
-
|
|
39582
|
-
|
|
39583
|
-
|
|
39584
|
-
|
|
39585
|
-
|
|
39586
|
-
|
|
39587
|
-
|
|
39588
|
-
|
|
39549
|
+
say(() => console.log(` Predicted address: ${predicted}`));
|
|
39550
|
+
const createAccountData = encodeFunctionData({
|
|
39551
|
+
abi: SailKernelAbi,
|
|
39552
|
+
functionName: "createAccount",
|
|
39553
|
+
args: [
|
|
39554
|
+
SAFE_V141.proxyFactory,
|
|
39555
|
+
SAFE_V141.singletonL2,
|
|
39556
|
+
initializer,
|
|
39557
|
+
saltNonce,
|
|
39558
|
+
ownerAddr,
|
|
39559
|
+
// permissionSigner = owner (same as original deployment)
|
|
39560
|
+
managerAddr,
|
|
39561
|
+
// manager = agent wallet
|
|
39562
|
+
deployment.standardFeePolicy,
|
|
39563
|
+
zeroAddress
|
|
39564
|
+
// feeAsset (native)
|
|
39565
|
+
]
|
|
39566
|
+
});
|
|
39567
|
+
const channel = await createSigningChannel(process.cwd());
|
|
39589
39568
|
try {
|
|
39590
|
-
|
|
39591
|
-
const
|
|
39592
|
-
|
|
39593
|
-
|
|
39594
|
-
|
|
39595
|
-
|
|
39569
|
+
await channel.start();
|
|
39570
|
+
const stationUrl = `http://localhost:${projectPort(process.cwd())}/#/station`;
|
|
39571
|
+
if (json) {
|
|
39572
|
+
console.log(
|
|
39573
|
+
JSON.stringify(
|
|
39574
|
+
{ status: "waiting_for_signature", url: stationUrl, chainId: targetChainId },
|
|
39575
|
+
null,
|
|
39576
|
+
2
|
|
39577
|
+
)
|
|
39578
|
+
);
|
|
39579
|
+
} else {
|
|
39580
|
+
console.log(
|
|
39581
|
+
`
|
|
39582
|
+
\u2192 Open the Sailor dashboard and switch your wallet to ${getChainById(targetChainId).name}:
|
|
39583
|
+
${stationUrl}
|
|
39584
|
+
`
|
|
39585
|
+
);
|
|
39596
39586
|
}
|
|
39597
|
-
|
|
39598
|
-
const
|
|
39599
|
-
|
|
39600
|
-
|
|
39601
|
-
|
|
39602
|
-
|
|
39603
|
-
|
|
39604
|
-
|
|
39605
|
-
|
|
39587
|
+
say(() => console.log("Pushing signing request\u2026"));
|
|
39588
|
+
const response = await channel.requestSignature({
|
|
39589
|
+
type: "transaction",
|
|
39590
|
+
kind: "create-sma",
|
|
39591
|
+
title: `Deploy SMA on ${getChainById(targetChainId).name}`,
|
|
39592
|
+
description: `Deploy the same SMA at ${predicted} on ${getChainById(targetChainId).name}. Switch your wallet to chain ${targetChainId} before signing.`,
|
|
39593
|
+
chainId: targetChainId,
|
|
39594
|
+
to: deployment.kernel,
|
|
39595
|
+
data: createAccountData,
|
|
39596
|
+
details: [
|
|
39597
|
+
{ label: "Owner", value: ownerAddr },
|
|
39598
|
+
{ label: "Agent wallet", value: managerAddr },
|
|
39599
|
+
{ label: "Predicted address", value: predicted },
|
|
39600
|
+
{ label: "Fee policy", value: deployment.standardFeePolicy },
|
|
39601
|
+
{ label: "Salt", value: saltNonce.toString() }
|
|
39602
|
+
]
|
|
39603
|
+
});
|
|
39604
|
+
if (response.status === "rejected") {
|
|
39605
|
+
throw new Error(`User rejected deployment: ${response.reason ?? "no reason given"}`);
|
|
39606
|
+
}
|
|
39607
|
+
if (response.status !== "signed") {
|
|
39608
|
+
throw new Error("Unexpected response from signing UI");
|
|
39609
|
+
}
|
|
39610
|
+
say(() => console.log("Waiting for transaction confirmation\u2026"));
|
|
39611
|
+
const receipt = await targetClient.waitForTransactionReceipt({ hash: response.txHash });
|
|
39612
|
+
const logs = parseEventLogs({ abi: SailKernelAbi, logs: receipt.logs });
|
|
39613
|
+
const registered = logs.find(
|
|
39614
|
+
(l) => l.eventName === "AccountRegistered"
|
|
39615
|
+
);
|
|
39616
|
+
if (!registered) {
|
|
39617
|
+
throw new Error(
|
|
39618
|
+
`AccountRegistered event not found in receipt (tx ${response.txHash}) \u2014 transaction may have failed or was sent to the wrong contract.`
|
|
39606
39619
|
);
|
|
39607
|
-
const mandates = JSON.parse(mandatesRaw);
|
|
39608
|
-
permissionCount = Array.isArray(mandates.mandates) ? mandates.mandates.length : 0;
|
|
39609
|
-
} catch {
|
|
39610
39620
|
}
|
|
39611
|
-
|
|
39612
|
-
|
|
39621
|
+
const deployedAddress = registered.args.account;
|
|
39622
|
+
if (deployedAddress.toLowerCase() !== predicted.toLowerCase()) {
|
|
39623
|
+
throw new Error(
|
|
39624
|
+
`Deployed address mismatch: predicted ${predicted}, got ${deployedAddress}.
|
|
39625
|
+
Please report this as a bug \u2014 this should not happen with deterministic contracts.`
|
|
39626
|
+
);
|
|
39613
39627
|
}
|
|
39614
|
-
|
|
39615
|
-
|
|
39616
|
-
|
|
39628
|
+
recordDeployedChain(stored, targetChainId);
|
|
39629
|
+
appendActivity({
|
|
39630
|
+
ts: nowIso(),
|
|
39631
|
+
actor: "owner",
|
|
39632
|
+
type: "sma_deployed_chain",
|
|
39633
|
+
sma: deployedAddress,
|
|
39634
|
+
owner: ownerAddr,
|
|
39635
|
+
manager: managerAddr,
|
|
39636
|
+
txHash: response.txHash,
|
|
39637
|
+
chainId: targetChainId,
|
|
39638
|
+
saltNonce: saltNonce.toString()
|
|
39639
|
+
});
|
|
39640
|
+
say(() => {
|
|
39641
|
+
console.log(`
|
|
39642
|
+
${"\u2500".repeat(56)}`);
|
|
39643
|
+
console.log("\u2713 SMA deployed on additional chain!");
|
|
39644
|
+
console.log(` Address: ${deployedAddress}`);
|
|
39645
|
+
console.log(` Chain: ${getChainById(targetChainId).name} (${targetChainId})`);
|
|
39646
|
+
console.log(` Tx: ${response.txHash}`);
|
|
39647
|
+
console.log("\u2500".repeat(56));
|
|
39648
|
+
});
|
|
39649
|
+
if (json) {
|
|
39650
|
+
console.log(
|
|
39651
|
+
JSON.stringify(
|
|
39652
|
+
{
|
|
39653
|
+
status: "ok",
|
|
39654
|
+
address: deployedAddress,
|
|
39655
|
+
chainId: targetChainId,
|
|
39656
|
+
txHash: response.txHash
|
|
39657
|
+
},
|
|
39658
|
+
null,
|
|
39659
|
+
2
|
|
39660
|
+
)
|
|
39661
|
+
);
|
|
39662
|
+
}
|
|
39663
|
+
} finally {
|
|
39664
|
+
channel.stop();
|
|
39617
39665
|
}
|
|
39618
39666
|
}
|
|
39619
|
-
function
|
|
39620
|
-
const
|
|
39621
|
-
|
|
39622
|
-
|
|
39623
|
-
|
|
39624
|
-
|
|
39625
|
-
|
|
39626
|
-
|
|
39627
|
-
|
|
39628
|
-
|
|
39629
|
-
|
|
39630
|
-
|
|
39631
|
-
|
|
39632
|
-
|
|
39633
|
-
|
|
39634
|
-
|
|
39635
|
-
|
|
39636
|
-
|
|
39637
|
-
|
|
39638
|
-
|
|
39639
|
-
|
|
39640
|
-
|
|
39641
|
-
);
|
|
39642
|
-
console.log(" forge build");
|
|
39643
|
-
console.log(` sailor mandate deploy --contract <Name> --attach --sma ${state.sma}
|
|
39644
|
-
`);
|
|
39645
|
-
console.log('Or open this folder in your AI coding assistant and say: "continue"');
|
|
39646
|
-
return;
|
|
39647
|
-
}
|
|
39648
|
-
if (state.kind === "D") {
|
|
39649
|
-
console.log("\nWelcome back.\n");
|
|
39650
|
-
console.log(`Project: ${state.projectName}`);
|
|
39651
|
-
console.log(`SMA: ${state.sma} on ${state.chain}`);
|
|
39652
|
-
console.log(`Permissions: ${state.permissionCount} registered
|
|
39653
|
-
`);
|
|
39654
|
-
console.log('Open this folder in your AI coding assistant and say: "continue"');
|
|
39655
|
-
return;
|
|
39667
|
+
function buildSmaAddressPrediction(deployment, ownerAddr, managerAddr, saltNonce, proxyCreationCode) {
|
|
39668
|
+
const initializer = buildSafeSetupInitializer({
|
|
39669
|
+
owners: [ownerAddr],
|
|
39670
|
+
threshold: 1n,
|
|
39671
|
+
kernel: deployment.kernel,
|
|
39672
|
+
safeModuleEnabler: deployment.safeModuleEnabler
|
|
39673
|
+
});
|
|
39674
|
+
const predicted = computeSailSmaAddress({
|
|
39675
|
+
initializer,
|
|
39676
|
+
saltNonce,
|
|
39677
|
+
deployer: ownerAddr,
|
|
39678
|
+
permissionSigner: ownerAddr,
|
|
39679
|
+
manager: managerAddr,
|
|
39680
|
+
feePolicy: deployment.standardFeePolicy,
|
|
39681
|
+
proxyCreationCode
|
|
39682
|
+
});
|
|
39683
|
+
return { initializer, predicted };
|
|
39684
|
+
}
|
|
39685
|
+
function recordDeployedChain(stored, chainId) {
|
|
39686
|
+
const existing = Array.from(/* @__PURE__ */ new Set([stored.chainId, ...stored.deployedChains ?? []]));
|
|
39687
|
+
if (!existing.includes(chainId)) {
|
|
39688
|
+
existing.push(chainId);
|
|
39689
|
+
existing.sort((a, b) => a - b);
|
|
39656
39690
|
}
|
|
39657
|
-
|
|
39658
|
-
|
|
39659
|
-
|
|
39691
|
+
const updated = { ...stored, deployedChains: existing };
|
|
39692
|
+
upsertAccountInList(updated);
|
|
39693
|
+
writeJsonFile(sailPath("account.json"), updated);
|
|
39660
39694
|
}
|
|
39661
39695
|
|
|
39662
|
-
// src/
|
|
39663
|
-
|
|
39664
|
-
|
|
39665
|
-
|
|
39666
|
-
|
|
39667
|
-
|
|
39668
|
-
if (!role) {
|
|
39669
|
-
throw new Error(`Unknown key role: "${roleInput}". Choose "agent wallet" or "mandate signer".`);
|
|
39696
|
+
// src/lib/output.ts
|
|
39697
|
+
function emit(json, human, payload) {
|
|
39698
|
+
if (json) {
|
|
39699
|
+
console.log(JSON.stringify(payload));
|
|
39700
|
+
} else {
|
|
39701
|
+
human();
|
|
39670
39702
|
}
|
|
39671
|
-
|
|
39672
|
-
|
|
39673
|
-
|
|
39674
|
-
|
|
39675
|
-
|
|
39676
|
-
|
|
39677
|
-
|
|
39703
|
+
}
|
|
39704
|
+
|
|
39705
|
+
// src/lib/project.ts
|
|
39706
|
+
init_esm2();
|
|
39707
|
+
function nonEmpty(value) {
|
|
39708
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
39709
|
+
}
|
|
39710
|
+
var ProjectContext = class {
|
|
39711
|
+
config;
|
|
39712
|
+
chainId;
|
|
39713
|
+
deployment;
|
|
39714
|
+
contracts;
|
|
39715
|
+
constructor() {
|
|
39716
|
+
const cfg = readJsonFile(sailPath("config.json"));
|
|
39717
|
+
if (!cfg) {
|
|
39718
|
+
throw new Error('No Sailor project found here. Run "sailor init" first.');
|
|
39678
39719
|
}
|
|
39720
|
+
this.config = cfg;
|
|
39721
|
+
this.chainId = cfg.chainId ?? 8453;
|
|
39722
|
+
this.deployment = getSailDeployment(this.chainId);
|
|
39723
|
+
const overrides = cfg.contracts ?? {};
|
|
39724
|
+
this.contracts = {
|
|
39725
|
+
chainId: this.chainId,
|
|
39726
|
+
kernel: getAddress(nonEmpty(overrides.kernel) ? overrides.kernel : this.deployment.kernel),
|
|
39727
|
+
governance: getAddress(
|
|
39728
|
+
nonEmpty(overrides.governance) ? overrides.governance : this.deployment.governance
|
|
39729
|
+
),
|
|
39730
|
+
standardFeePolicy: getAddress(
|
|
39731
|
+
nonEmpty(overrides.standardFeePolicy) ? overrides.standardFeePolicy : this.deployment.standardFeePolicy
|
|
39732
|
+
),
|
|
39733
|
+
safeModuleEnabler: getAddress(
|
|
39734
|
+
nonEmpty(overrides.safeModuleEnabler) ? overrides.safeModuleEnabler : this.deployment.safeModuleEnabler
|
|
39735
|
+
),
|
|
39736
|
+
// Accept both override names: mandateFactory (new) and permissionFactory (legacy).
|
|
39737
|
+
mandateFactory: getAddress(
|
|
39738
|
+
nonEmpty(overrides.mandateFactory) ? overrides.mandateFactory : nonEmpty(overrides.permissionFactory) ? overrides.permissionFactory : this.deployment.mandateFactory
|
|
39739
|
+
)
|
|
39740
|
+
};
|
|
39679
39741
|
}
|
|
39680
|
-
|
|
39681
|
-
|
|
39682
|
-
throw new Error("Password must be at least 8 characters.");
|
|
39742
|
+
static exists() {
|
|
39743
|
+
return fileExists(sailPath("config.json"));
|
|
39683
39744
|
}
|
|
39684
|
-
|
|
39685
|
-
|
|
39686
|
-
throw new Error("Passwords do not match.");
|
|
39745
|
+
get name() {
|
|
39746
|
+
return this.config.name ?? "sailor-agent";
|
|
39687
39747
|
}
|
|
39688
|
-
|
|
39689
|
-
|
|
39690
|
-
|
|
39691
|
-
|
|
39692
|
-
|
|
39693
|
-
|
|
39694
|
-
|
|
39695
|
-
|
|
39696
|
-
|
|
39697
|
-
|
|
39698
|
-
|
|
39699
|
-
|
|
39700
|
-
|
|
39701
|
-
|
|
39702
|
-
|
|
39703
|
-
|
|
39704
|
-
|
|
39705
|
-
|
|
39706
|
-
|
|
39707
|
-
|
|
39708
|
-
|
|
39709
|
-
import_node_fs7.default.writeFileSync(envPath, content, { mode: 384 });
|
|
39710
|
-
console.log("\u2713 SAIL_PASSPHRASE saved to .sail/.env.local (mode 0600)");
|
|
39711
|
-
console.log(" sailor run will now work non-interactively.");
|
|
39712
|
-
} else {
|
|
39713
|
-
console.log("\nTo run non-interactively, add this to .sail/.env.local:");
|
|
39714
|
-
console.log(` SAIL_PASSPHRASE=<your-passphrase>`);
|
|
39748
|
+
// ── Owner persistence (.sail/state/owner.json) ──────────────────────────────
|
|
39749
|
+
getOwner() {
|
|
39750
|
+
const state = readJsonFile(sailPath("state", "owner.json"));
|
|
39751
|
+
if (state?.owner) return getAddress(state.owner);
|
|
39752
|
+
const account2 = readJsonFile(sailPath("account.json"));
|
|
39753
|
+
return account2?.owner ? getAddress(account2.owner) : null;
|
|
39754
|
+
}
|
|
39755
|
+
setOwner(owner2) {
|
|
39756
|
+
writeJsonFile(sailPath("state", "owner.json"), {
|
|
39757
|
+
owner: getAddress(owner2),
|
|
39758
|
+
chainId: this.chainId,
|
|
39759
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
39760
|
+
});
|
|
39761
|
+
}
|
|
39762
|
+
};
|
|
39763
|
+
async function loadManagerSigner2() {
|
|
39764
|
+
if (!process.env.SAIL_PASSPHRASE) {
|
|
39765
|
+
try {
|
|
39766
|
+
const env = parseEnvFile(sailPath(".env.local"));
|
|
39767
|
+
if (env.SAIL_PASSPHRASE) process.env.SAIL_PASSPHRASE = env.SAIL_PASSPHRASE;
|
|
39768
|
+
} catch {
|
|
39715
39769
|
}
|
|
39716
39770
|
}
|
|
39717
|
-
|
|
39718
|
-
|
|
39719
|
-
|
|
39720
|
-
|
|
39721
|
-
|
|
39722
|
-
throw new Error(
|
|
39723
|
-
'No agent wallet keystore found.\nComplete Stage 1 (browser UI) to generate your agent wallet, or run\n"sailor keys generate" and choose "agent wallet" to create one manually.'
|
|
39724
|
-
);
|
|
39725
|
-
}
|
|
39726
|
-
const dest = import_node_path6.default.resolve(process.cwd(), "ci-keystore.json");
|
|
39727
|
-
import_node_fs7.default.copyFileSync(src, dest);
|
|
39728
|
-
console.log(`\u2713 Keystore copied to ci-keystore.json`);
|
|
39729
|
-
console.log(` Source: ${src}`);
|
|
39730
|
-
const gitignorePath = import_node_path6.default.resolve(process.cwd(), ".gitignore");
|
|
39731
|
-
if (import_node_fs7.default.existsSync(gitignorePath)) {
|
|
39732
|
-
const content = import_node_fs7.default.readFileSync(gitignorePath, "utf-8");
|
|
39733
|
-
if (!content.includes("ci-keystore.json")) {
|
|
39734
|
-
import_node_fs7.default.appendFileSync(
|
|
39735
|
-
gitignorePath,
|
|
39736
|
-
"\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
|
|
39737
|
-
);
|
|
39738
|
-
console.log("\u2713 Added !ci-keystore.json allowlist entry to .gitignore");
|
|
39739
|
-
} else {
|
|
39740
|
-
console.log("\u2713 .gitignore already tracks ci-keystore.json");
|
|
39771
|
+
const passphrase = process.env.SAIL_PASSPHRASE;
|
|
39772
|
+
if (passphrase) {
|
|
39773
|
+
const keystore = readJsonFile(keyPath("manager"));
|
|
39774
|
+
if (!keystore) {
|
|
39775
|
+
throw new Error('No manager key found.\nRun "sailor keys generate" and choose "manager".');
|
|
39741
39776
|
}
|
|
39777
|
+
return LocalKeyring.fromKeystore(keystore, passphrase);
|
|
39742
39778
|
}
|
|
39743
|
-
|
|
39744
|
-
console.log(" 1. Add two GitHub Actions secrets (Settings \u2192 Secrets \u2192 Actions):");
|
|
39745
|
-
console.log(" SAIL_PASSPHRASE \u2014 the passphrase that encrypts your agent wallet");
|
|
39746
|
-
console.log(" RPC_URL \u2014 your RPC endpoint");
|
|
39747
|
-
console.log(" 2. Commit and push ci-keystore.json:");
|
|
39748
|
-
console.log(' git add ci-keystore.json && git commit -m "chore: add CI keystore" && git push');
|
|
39749
|
-
console.log("\n The keystore is encrypted \u2014 the raw private key is never exposed.");
|
|
39750
|
-
console.log(" The workflow at .github/workflows/agent-tick.yml unlocks it with SAIL_PASSPHRASE.");
|
|
39779
|
+
return loadKeyring("manager");
|
|
39751
39780
|
}
|
|
39752
|
-
|
|
39753
|
-
|
|
39754
|
-
|
|
39755
|
-
|
|
39756
|
-
|
|
39757
|
-
|
|
39781
|
+
|
|
39782
|
+
// src/commands/capabilities.ts
|
|
39783
|
+
function chainName(chainId) {
|
|
39784
|
+
try {
|
|
39785
|
+
return getChainById(chainId).name;
|
|
39786
|
+
} catch {
|
|
39787
|
+
return `Chain ${chainId}`;
|
|
39758
39788
|
}
|
|
39759
|
-
|
|
39760
|
-
|
|
39761
|
-
|
|
39762
|
-
|
|
39763
|
-
|
|
39764
|
-
|
|
39765
|
-
|
|
39766
|
-
|
|
39789
|
+
}
|
|
39790
|
+
async function capabilities(options = {}) {
|
|
39791
|
+
const project = new ProjectContext();
|
|
39792
|
+
const chainId = project.chainId;
|
|
39793
|
+
const kernel = project.contracts.kernel;
|
|
39794
|
+
const deployment = getSailDeployment(chainId);
|
|
39795
|
+
const rpcUrl = getRpcUrl(chainId) ?? getChainById(chainId).rpcUrls.default.http[0];
|
|
39796
|
+
let dispatchModel = deployment.dispatchModel;
|
|
39797
|
+
let modelSource = "static-hint";
|
|
39798
|
+
try {
|
|
39799
|
+
const caps = await new SailorClient({ chainId, rpcUrl, kernel }).capabilities();
|
|
39800
|
+
dispatchModel = caps.dispatchModel;
|
|
39801
|
+
modelSource = caps.source;
|
|
39802
|
+
} catch {
|
|
39767
39803
|
}
|
|
39804
|
+
const cloneTemplates = (deployment.cloneTemplates ?? []).map((t) => ({
|
|
39805
|
+
key: t.key,
|
|
39806
|
+
kind: t.kind,
|
|
39807
|
+
label: t.label,
|
|
39808
|
+
description: t.description,
|
|
39809
|
+
address: t.address,
|
|
39810
|
+
initParams: t.initParams
|
|
39811
|
+
}));
|
|
39812
|
+
const knownTemplates = (deployment.knownTemplates ?? []).map((t) => ({
|
|
39813
|
+
kind: t.kind,
|
|
39814
|
+
label: t.label,
|
|
39815
|
+
description: t.description,
|
|
39816
|
+
address: t.address
|
|
39817
|
+
}));
|
|
39818
|
+
const bareTemplates = Object.keys(deployment.standaloneTemplates ?? {}).filter(
|
|
39819
|
+
(k) => !cloneTemplates.some((c) => c.key === k)
|
|
39820
|
+
);
|
|
39821
|
+
const strategyPrimitives = [
|
|
39822
|
+
"strategy.swap \u2014 bounded swap (one-off, or looped on a schedule for DCA/rebalance)",
|
|
39823
|
+
"dispatch.single \u2014 a single permitted call through the kernel",
|
|
39824
|
+
dispatchModel === "selective" ? "dispatch.batch / dispatch.preview \u2014 multi-call (selective kernels only)" : "dispatch.batch / dispatch.preview \u2014 UNAVAILABLE on this conjunctive kernel"
|
|
39825
|
+
];
|
|
39826
|
+
const payload = {
|
|
39827
|
+
chainId,
|
|
39828
|
+
chainName: chainName(chainId),
|
|
39829
|
+
supported: true,
|
|
39830
|
+
dispatchModel,
|
|
39831
|
+
dispatchModelSource: modelSource,
|
|
39832
|
+
contracts: {
|
|
39833
|
+
kernel,
|
|
39834
|
+
mandateFactory: project.contracts.mandateFactory
|
|
39835
|
+
},
|
|
39836
|
+
supportedChains: Object.keys(sailDeployments).map((id) => ({
|
|
39837
|
+
chainId: Number(id),
|
|
39838
|
+
name: chainName(Number(id)),
|
|
39839
|
+
dispatchModel: sailDeployments[id].dispatchModel
|
|
39840
|
+
})),
|
|
39841
|
+
mandateTemplates: {
|
|
39842
|
+
// No-Solidity, self-describing clone templates (deployAndAttach + initialize).
|
|
39843
|
+
cloneTemplates,
|
|
39844
|
+
// Pre-deployed shared permissions.
|
|
39845
|
+
knownTemplates,
|
|
39846
|
+
// Deployable clone logic without rich wizard metadata yet.
|
|
39847
|
+
otherStandaloneTemplates: bareTemplates
|
|
39848
|
+
},
|
|
39849
|
+
strategyPrimitives,
|
|
39850
|
+
customMandates: "Author a Foundry IPermission contract under mandates/ when no template fits; keep all policy parameters constructor-configured.",
|
|
39851
|
+
intelligence: {
|
|
39852
|
+
baseUrl: SAIL_INTELLIGENCE_BASE_URL,
|
|
39853
|
+
docsUrl: SAIL_INTELLIGENCE_DOCS_URL,
|
|
39854
|
+
use: "Vault screening, allocation, and rebalance advice for yield strategies."
|
|
39855
|
+
}
|
|
39856
|
+
};
|
|
39857
|
+
emit(
|
|
39858
|
+
options.json,
|
|
39859
|
+
() => {
|
|
39860
|
+
console.log("Sailor capabilities");
|
|
39861
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
39862
|
+
console.log(`Chain: ${payload.chainName} (${chainId})`);
|
|
39863
|
+
console.log(`Dispatch model: ${dispatchModel ?? "unknown"} (${modelSource})`);
|
|
39864
|
+
console.log(
|
|
39865
|
+
`Supported chains: ${payload.supportedChains.map((c) => `${c.name} [${c.dispatchModel}]`).join(", ")}`
|
|
39866
|
+
);
|
|
39867
|
+
console.log("\nNo-Solidity mandate templates on this chain:");
|
|
39868
|
+
if (cloneTemplates.length === 0 && knownTemplates.length === 0 && bareTemplates.length === 0) {
|
|
39869
|
+
console.log(" (none registered for this chain yet \u2014 author a custom mandate)");
|
|
39870
|
+
}
|
|
39871
|
+
for (const t of cloneTemplates) {
|
|
39872
|
+
console.log(` \u2022 ${t.label} (${t.kind}) \u2014 ${t.description ?? ""}`);
|
|
39873
|
+
console.log(` params: ${t.initParams.map((p) => `${p.name}: ${p.type}`).join(", ")}`);
|
|
39874
|
+
}
|
|
39875
|
+
for (const t of knownTemplates) {
|
|
39876
|
+
console.log(` \u2022 ${t.label} (${t.kind}, shared) \u2014 ${t.description ?? ""}`);
|
|
39877
|
+
}
|
|
39878
|
+
if (bareTemplates.length > 0) {
|
|
39879
|
+
console.log(` \u2022 also deployable: ${bareTemplates.join(", ")}`);
|
|
39880
|
+
}
|
|
39881
|
+
console.log("\nStrategy primitives:");
|
|
39882
|
+
for (const p of strategyPrimitives) console.log(` \u2022 ${p}`);
|
|
39883
|
+
console.log("\nCustom mandates:");
|
|
39884
|
+
console.log(` ${payload.customMandates}`);
|
|
39885
|
+
console.log("\nIntelligence API (yield/allocation advice):");
|
|
39886
|
+
console.log(` ${SAIL_INTELLIGENCE_BASE_URL} (docs: ${SAIL_INTELLIGENCE_DOCS_URL})`);
|
|
39887
|
+
console.log(
|
|
39888
|
+
"\nUse this to decide if a request is buildable. If it can't be expressed as a template, a strategy primitive, or a custom mandate, say so \u2014 don't scaffold a revert."
|
|
39889
|
+
);
|
|
39890
|
+
},
|
|
39891
|
+
payload
|
|
39892
|
+
);
|
|
39768
39893
|
}
|
|
39769
39894
|
|
|
39770
|
-
// src/commands/
|
|
39771
|
-
var import_node_child_process = require("node:child_process");
|
|
39772
|
-
var import_node_fs10 = require("node:fs");
|
|
39773
|
-
var import_node_path9 = require("node:path");
|
|
39895
|
+
// src/commands/doctor.ts
|
|
39774
39896
|
init_esm2();
|
|
39775
39897
|
|
|
39776
|
-
// src/lib/
|
|
39777
|
-
|
|
39778
|
-
|
|
39779
|
-
|
|
39780
|
-
|
|
39781
|
-
}
|
|
39782
|
-
|
|
39783
|
-
const parsed = readJsonFile(this.filePath);
|
|
39784
|
-
return { version: 1, mandates: parsed?.mandates ?? [] };
|
|
39785
|
-
}
|
|
39786
|
-
write(data) {
|
|
39787
|
-
writeJsonFile(this.filePath, data);
|
|
39898
|
+
// src/lib/contract-check.ts
|
|
39899
|
+
async function checkContractExists(pc, address) {
|
|
39900
|
+
try {
|
|
39901
|
+
const code = await pc.getCode({ address });
|
|
39902
|
+
return { address, hasCode: !!code && code !== "0x" };
|
|
39903
|
+
} catch (err) {
|
|
39904
|
+
return { address, hasCode: false, error: err.message.split("\n")[0] };
|
|
39788
39905
|
}
|
|
39789
|
-
|
|
39790
|
-
|
|
39906
|
+
}
|
|
39907
|
+
|
|
39908
|
+
// src/lib/permission-resolver.ts
|
|
39909
|
+
var IPERMISSION_ABI = [
|
|
39910
|
+
{
|
|
39911
|
+
type: "function",
|
|
39912
|
+
name: "evaluate",
|
|
39913
|
+
stateMutability: "view",
|
|
39914
|
+
inputs: [
|
|
39915
|
+
{ name: "txData", type: "bytes" },
|
|
39916
|
+
{
|
|
39917
|
+
name: "ctx",
|
|
39918
|
+
type: "tuple",
|
|
39919
|
+
components: [
|
|
39920
|
+
{ name: "account", type: "address" },
|
|
39921
|
+
{ name: "manager", type: "address" },
|
|
39922
|
+
{ name: "submitter", type: "address" },
|
|
39923
|
+
{ name: "target", type: "address" },
|
|
39924
|
+
{ name: "selector", type: "bytes4" },
|
|
39925
|
+
{ name: "value", type: "uint256" },
|
|
39926
|
+
{ name: "blockTimestamp", type: "uint256" },
|
|
39927
|
+
{ name: "blockNumber", type: "uint256" }
|
|
39928
|
+
]
|
|
39929
|
+
}
|
|
39930
|
+
],
|
|
39931
|
+
outputs: [{ type: "bool" }]
|
|
39791
39932
|
}
|
|
39792
|
-
|
|
39793
|
-
|
|
39794
|
-
|
|
39795
|
-
|
|
39796
|
-
|
|
39797
|
-
|
|
39933
|
+
];
|
|
39934
|
+
function buildPermissionContext(params) {
|
|
39935
|
+
const { account: account2, manager, call: call2, blockInfo } = params;
|
|
39936
|
+
const selector = call2.data.length >= 10 ? call2.data.slice(0, 10) : "0x00000000";
|
|
39937
|
+
return {
|
|
39938
|
+
account: account2,
|
|
39939
|
+
manager,
|
|
39940
|
+
submitter: manager,
|
|
39941
|
+
// runner submits dispatches from the manager (agent) wallet
|
|
39942
|
+
target: call2.target,
|
|
39943
|
+
selector,
|
|
39944
|
+
value: call2.value,
|
|
39945
|
+
blockTimestamp: blockInfo.timestamp,
|
|
39946
|
+
blockNumber: blockInfo.number
|
|
39947
|
+
};
|
|
39948
|
+
}
|
|
39949
|
+
async function probePermissionForCall(params) {
|
|
39950
|
+
const { publicClient, permission, account: account2, manager, call: call2, blockInfo } = params;
|
|
39951
|
+
const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
|
|
39952
|
+
try {
|
|
39953
|
+
const accepted = await publicClient.readContract({
|
|
39954
|
+
address: permission,
|
|
39955
|
+
abi: IPERMISSION_ABI,
|
|
39956
|
+
functionName: "evaluate",
|
|
39957
|
+
args: [call2.data, ctx]
|
|
39958
|
+
});
|
|
39959
|
+
return { accepted: Boolean(accepted), reverted: false };
|
|
39960
|
+
} catch (err) {
|
|
39961
|
+
return { accepted: false, reverted: true, error: err.message.split("\n")[0] };
|
|
39798
39962
|
}
|
|
39799
|
-
|
|
39800
|
-
|
|
39801
|
-
|
|
39802
|
-
|
|
39803
|
-
|
|
39804
|
-
|
|
39805
|
-
|
|
39806
|
-
|
|
39963
|
+
}
|
|
39964
|
+
async function resolvePermissionForCall(params) {
|
|
39965
|
+
const { publicClient, account: account2, manager, call: call2, registeredPermissions, blockInfo } = params;
|
|
39966
|
+
const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
|
|
39967
|
+
for (const permission of registeredPermissions) {
|
|
39968
|
+
try {
|
|
39969
|
+
const accepted = await publicClient.readContract({
|
|
39970
|
+
address: permission,
|
|
39971
|
+
abi: IPERMISSION_ABI,
|
|
39972
|
+
functionName: "evaluate",
|
|
39973
|
+
args: [call2.data, ctx]
|
|
39974
|
+
});
|
|
39975
|
+
if (accepted) return permission;
|
|
39976
|
+
} catch {
|
|
39977
|
+
}
|
|
39807
39978
|
}
|
|
39808
|
-
|
|
39809
|
-
|
|
39810
|
-
|
|
39811
|
-
|
|
39812
|
-
|
|
39813
|
-
|
|
39814
|
-
|
|
39815
|
-
|
|
39979
|
+
return void 0;
|
|
39980
|
+
}
|
|
39981
|
+
async function resolvePermissionForBatch(params) {
|
|
39982
|
+
const { publicClient, kernel, account: account2, calls, registeredPermissions } = params;
|
|
39983
|
+
for (const permission of registeredPermissions) {
|
|
39984
|
+
try {
|
|
39985
|
+
const [approved] = await publicClient.readContract({
|
|
39986
|
+
address: kernel,
|
|
39987
|
+
abi: SailKernelAbi,
|
|
39988
|
+
functionName: "previewBatch",
|
|
39989
|
+
args: [account2, permission, calls]
|
|
39990
|
+
});
|
|
39991
|
+
if (approved) return permission;
|
|
39992
|
+
} catch {
|
|
39993
|
+
}
|
|
39816
39994
|
}
|
|
39817
|
-
|
|
39818
|
-
|
|
39819
|
-
// src/signing/client.ts
|
|
39820
|
-
var import_node_fs9 = require("node:fs");
|
|
39821
|
-
var import_node_path8 = require("node:path");
|
|
39822
|
-
|
|
39823
|
-
// src/signing/server.ts
|
|
39824
|
-
var import_node_crypto2 = require("node:crypto");
|
|
39825
|
-
var import_node_fs8 = require("node:fs");
|
|
39826
|
-
var import_node_http = require("node:http");
|
|
39827
|
-
var import_node_net = require("node:net");
|
|
39828
|
-
var import_node_path7 = require("node:path");
|
|
39829
|
-
|
|
39830
|
-
// ../../node_modules/.pnpm/ws@8.21.0_bufferutil@4.1.0_utf-8-validate@5.0.10/node_modules/ws/wrapper.mjs
|
|
39831
|
-
var import_stream2 = __toESM(require_stream2(), 1);
|
|
39832
|
-
var import_extension2 = __toESM(require_extension2(), 1);
|
|
39833
|
-
var import_permessage_deflate2 = __toESM(require_permessage_deflate2(), 1);
|
|
39834
|
-
var import_receiver2 = __toESM(require_receiver2(), 1);
|
|
39835
|
-
var import_sender2 = __toESM(require_sender2(), 1);
|
|
39836
|
-
var import_subprotocol2 = __toESM(require_subprotocol2(), 1);
|
|
39837
|
-
var import_websocket2 = __toESM(require_websocket2(), 1);
|
|
39838
|
-
var import_websocket_server2 = __toESM(require_websocket_server2(), 1);
|
|
39995
|
+
return void 0;
|
|
39996
|
+
}
|
|
39839
39997
|
|
|
39840
|
-
// src/
|
|
39841
|
-
var
|
|
39842
|
-
|
|
39843
|
-
|
|
39844
|
-
|
|
39845
|
-
|
|
39846
|
-
|
|
39847
|
-
|
|
39848
|
-
|
|
39849
|
-
|
|
39850
|
-
|
|
39851
|
-
|
|
39852
|
-
|
|
39853
|
-
|
|
39854
|
-
|
|
39855
|
-
|
|
39856
|
-
|
|
39857
|
-
|
|
39858
|
-
|
|
39859
|
-
|
|
39860
|
-
|
|
39861
|
-
|
|
39862
|
-
|
|
39863
|
-
|
|
39864
|
-
|
|
39865
|
-
|
|
39866
|
-
|
|
39998
|
+
// src/commands/doctor.ts
|
|
39999
|
+
var LOW_GAS_THRESHOLD_WEI = 500000000000000n;
|
|
40000
|
+
async function nativeBalance(pc, address) {
|
|
40001
|
+
const wei = await pc.getBalance({ address });
|
|
40002
|
+
return {
|
|
40003
|
+
address,
|
|
40004
|
+
wei: wei.toString(),
|
|
40005
|
+
eth: formatEther(wei),
|
|
40006
|
+
funded: wei > 0n,
|
|
40007
|
+
low: wei > 0n && wei < LOW_GAS_THRESHOLD_WEI
|
|
40008
|
+
};
|
|
40009
|
+
}
|
|
40010
|
+
function keystoreAddress(role, safe) {
|
|
40011
|
+
const ks = readJsonFile(resolveKeyPath(role, safe));
|
|
40012
|
+
return ks?.address ? getAddress(`0x${ks.address.replace(/^0x/, "")}`) : null;
|
|
40013
|
+
}
|
|
40014
|
+
var PROBE_TARGET = "0x000000000000000000000000000000000000dEaD";
|
|
40015
|
+
var PROBE_SELECTOR = "0xffffffff";
|
|
40016
|
+
var PROBE_DATA = "0xffffffff";
|
|
40017
|
+
async function probePassThrough(pc, permission, account2) {
|
|
40018
|
+
try {
|
|
40019
|
+
const ok = await pc.readContract({
|
|
40020
|
+
address: permission,
|
|
40021
|
+
abi: IPERMISSION_ABI,
|
|
40022
|
+
functionName: "evaluate",
|
|
40023
|
+
args: [
|
|
40024
|
+
PROBE_DATA,
|
|
40025
|
+
{
|
|
40026
|
+
account: account2,
|
|
40027
|
+
manager: account2,
|
|
40028
|
+
submitter: account2,
|
|
40029
|
+
target: PROBE_TARGET,
|
|
40030
|
+
selector: PROBE_SELECTOR,
|
|
40031
|
+
value: 0n,
|
|
40032
|
+
blockTimestamp: 0n,
|
|
40033
|
+
blockNumber: 0n
|
|
40034
|
+
}
|
|
40035
|
+
]
|
|
40036
|
+
});
|
|
40037
|
+
return { permission, passesThrough: ok };
|
|
40038
|
+
} catch (err) {
|
|
40039
|
+
return {
|
|
40040
|
+
permission,
|
|
40041
|
+
passesThrough: false,
|
|
40042
|
+
note: `evaluate reverted (${err.message.split("\n")[0]})`
|
|
40043
|
+
};
|
|
39867
40044
|
}
|
|
39868
|
-
return null;
|
|
39869
40045
|
}
|
|
39870
|
-
|
|
39871
|
-
|
|
39872
|
-
|
|
39873
|
-
|
|
39874
|
-
|
|
39875
|
-
|
|
39876
|
-
|
|
39877
|
-
|
|
39878
|
-
|
|
39879
|
-
|
|
39880
|
-
|
|
39881
|
-
|
|
39882
|
-
|
|
39883
|
-
|
|
39884
|
-
|
|
39885
|
-
|
|
39886
|
-
|
|
39887
|
-
|
|
39888
|
-
|
|
39889
|
-
|
|
39890
|
-
|
|
39891
|
-
|
|
39892
|
-
|
|
39893
|
-
advertise;
|
|
39894
|
-
/** Random secret generated at startup. Required on POST /requests to prevent
|
|
39895
|
-
* cross-origin pages from injecting signing requests. */
|
|
39896
|
-
requestSecret = "";
|
|
39897
|
-
constructor(opts = {}) {
|
|
39898
|
-
this.projectRoot = opts.projectRoot ?? process.cwd();
|
|
39899
|
-
this.runtimeDir = (0, import_node_path7.join)(this.projectRoot, RUNTIME_SUBDIR);
|
|
39900
|
-
this.port = opts.port ?? DEFAULT_SIGNING_PORT;
|
|
39901
|
-
this.uiDist = opts.uiDist ?? findUiDist();
|
|
39902
|
-
this.advertise = opts.advertise ?? true;
|
|
40046
|
+
async function doctor(options = {}) {
|
|
40047
|
+
const project = new ProjectContext();
|
|
40048
|
+
const chainId = project.chainId;
|
|
40049
|
+
const kernel = project.contracts.kernel;
|
|
40050
|
+
const rpcUrl = getRpcUrl(chainId) ?? getChainById(chainId).rpcUrls.default.http[0];
|
|
40051
|
+
const client = new SailorClient({ chainId, rpcUrl, kernel });
|
|
40052
|
+
const pc = createPublicClient({ chain: getChainById(chainId), transport: http(rpcUrl) });
|
|
40053
|
+
const caps = await client.capabilities();
|
|
40054
|
+
const stored = readJsonFile(sailPath("account.json"));
|
|
40055
|
+
const safe = options.account ? getAddress(options.account) : stored?.safe ? getAddress(stored.safe) : null;
|
|
40056
|
+
let permissions = [];
|
|
40057
|
+
let checks = [];
|
|
40058
|
+
let permsNoCode = [];
|
|
40059
|
+
if (safe) {
|
|
40060
|
+
const mandates = await client.mandate.list(safe);
|
|
40061
|
+
permissions = mandates.map((m) => getAddress(m.permission));
|
|
40062
|
+
if (permissions.length > 0) {
|
|
40063
|
+
const codeChecks = await Promise.all(permissions.map((p) => checkContractExists(pc, p)));
|
|
40064
|
+
permsNoCode = codeChecks.filter((c) => !c.hasCode && !c.error).map((c) => c.address);
|
|
40065
|
+
}
|
|
40066
|
+
if (caps.dispatchModel === "conjunctive" && permissions.length > 0) {
|
|
40067
|
+
checks = await Promise.all(permissions.map((p) => probePassThrough(pc, p, safe)));
|
|
40068
|
+
}
|
|
39903
40069
|
}
|
|
39904
|
-
|
|
39905
|
-
|
|
40070
|
+
const bricking = checks.filter((c) => c.passesThrough === false);
|
|
40071
|
+
const healthy = safe !== null && bricking.length === 0;
|
|
40072
|
+
const ownerAddr = stored?.owner ? getAddress(stored.owner) : project.getOwner();
|
|
40073
|
+
const managerAddr = stored?.manager ? getAddress(stored.manager) : keystoreAddress("manager", stored?.safe);
|
|
40074
|
+
let chainIdOnChain = null;
|
|
40075
|
+
try {
|
|
40076
|
+
chainIdOnChain = await pc.getChainId();
|
|
40077
|
+
} catch {
|
|
39906
40078
|
}
|
|
39907
|
-
|
|
39908
|
-
|
|
40079
|
+
const chainIdMatches = chainIdOnChain === null ? null : chainIdOnChain === chainId;
|
|
40080
|
+
let ownerBal = null;
|
|
40081
|
+
let managerBal = null;
|
|
40082
|
+
try {
|
|
40083
|
+
if (ownerAddr) ownerBal = await nativeBalance(pc, ownerAddr);
|
|
40084
|
+
if (managerAddr) managerBal = await nativeBalance(pc, managerAddr);
|
|
40085
|
+
} catch {
|
|
39909
40086
|
}
|
|
39910
|
-
|
|
39911
|
-
|
|
40087
|
+
if (options.json) {
|
|
40088
|
+
console.log(
|
|
40089
|
+
JSON.stringify(
|
|
40090
|
+
{
|
|
40091
|
+
chainId,
|
|
40092
|
+
kernel,
|
|
40093
|
+
dispatchModel: caps.dispatchModel,
|
|
40094
|
+
dispatchTypehash: caps.dispatchTypehash,
|
|
40095
|
+
capabilitySource: caps.source,
|
|
40096
|
+
rpc: { chainIdOnChain, chainIdMatches },
|
|
40097
|
+
wallet: {
|
|
40098
|
+
owner: ownerAddr ? { address: ownerAddr, ...ownerBal ?? {} } : null,
|
|
40099
|
+
manager: managerAddr ? { address: managerAddr, ...managerBal ?? {} } : null
|
|
40100
|
+
},
|
|
40101
|
+
account: safe,
|
|
40102
|
+
saltNonce: stored?.saltNonce ?? null,
|
|
40103
|
+
permissions,
|
|
40104
|
+
permissionsWithoutCode: permsNoCode,
|
|
40105
|
+
conjunctivePassThrough: caps.dispatchModel === "conjunctive" ? checks.map((c) => ({
|
|
40106
|
+
permission: c.permission,
|
|
40107
|
+
passesThrough: c.passesThrough,
|
|
40108
|
+
note: c.note
|
|
40109
|
+
})) : "n/a (selective kernel)",
|
|
40110
|
+
healthy
|
|
40111
|
+
},
|
|
40112
|
+
null,
|
|
40113
|
+
2
|
|
40114
|
+
)
|
|
40115
|
+
);
|
|
40116
|
+
return;
|
|
39912
40117
|
}
|
|
39913
|
-
|
|
39914
|
-
|
|
39915
|
-
|
|
39916
|
-
|
|
39917
|
-
|
|
39918
|
-
|
|
39919
|
-
|
|
39920
|
-
|
|
39921
|
-
|
|
39922
|
-
|
|
39923
|
-
|
|
39924
|
-
}
|
|
39925
|
-
this.handleConnection(ws);
|
|
39926
|
-
});
|
|
39927
|
-
await new Promise((res, rej) => {
|
|
39928
|
-
http2.listen(this.port, "127.0.0.1", res);
|
|
39929
|
-
http2.once("error", rej);
|
|
39930
|
-
});
|
|
39931
|
-
this.httpServer = http2;
|
|
39932
|
-
if (this.advertise) this.writeRuntimeState();
|
|
39933
|
-
process.once("SIGINT", () => this.stop());
|
|
39934
|
-
process.once("SIGTERM", () => this.stop());
|
|
40118
|
+
const multiBricking = bricking.length > 0 && permissions.length > 1;
|
|
40119
|
+
if (!safe) {
|
|
40120
|
+
console.log('\u2717 Setup incomplete \u2014 no SMA found. Run "sailor onboard --new-sma" to deploy one.');
|
|
40121
|
+
} else if (permissions.length === 0) {
|
|
40122
|
+
console.log("\u2717 Setup incomplete \u2014 no permissions registered. Your agent cannot dispatch until at least one permission is attached.");
|
|
40123
|
+
} else if (multiBricking) {
|
|
40124
|
+
console.log(`\u2717 Setup issue \u2014 ${bricking.length} of ${permissions.length} permissions block unrelated calls (see below).`);
|
|
40125
|
+
} else {
|
|
40126
|
+
console.log(
|
|
40127
|
+
"\u2713 Everything looks good \u2014 your SMA is deployed, your permission is registered,\n and your agent is authorized to dispatch."
|
|
40128
|
+
);
|
|
39935
40129
|
}
|
|
39936
|
-
|
|
39937
|
-
|
|
39938
|
-
|
|
39939
|
-
|
|
39940
|
-
|
|
39941
|
-
|
|
39942
|
-
|
|
40130
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
40131
|
+
console.log(`Chain: ${chainId}`);
|
|
40132
|
+
console.log(`Kernel: ${kernel}`);
|
|
40133
|
+
console.log(` dispatch model: ${caps.dispatchModel} (detected via ${caps.source})`);
|
|
40134
|
+
console.log(` DISPATCH_TYPEHASH: ${caps.dispatchTypehash}`);
|
|
40135
|
+
console.log("\nWallet & gas:");
|
|
40136
|
+
if (chainIdMatches === false) {
|
|
40137
|
+
console.log(
|
|
40138
|
+
` \u2717 RPC serves chain ${chainIdOnChain}, but the project is configured for ${chainId}. Fix RPC_URL in .sail/.env.local before doing anything.`
|
|
40139
|
+
);
|
|
40140
|
+
} else if (chainIdMatches === true) {
|
|
40141
|
+
console.log(` \u2713 RPC serves the configured chain (${chainId}).`);
|
|
40142
|
+
}
|
|
40143
|
+
const showBalance = (label, addr, bal) => {
|
|
40144
|
+
if (!addr) {
|
|
40145
|
+
console.log(` ${label}: not set`);
|
|
40146
|
+
return;
|
|
39943
40147
|
}
|
|
39944
|
-
|
|
39945
|
-
|
|
39946
|
-
|
|
39947
|
-
ws.close();
|
|
39948
|
-
} catch {
|
|
39949
|
-
}
|
|
40148
|
+
if (!bal) {
|
|
40149
|
+
console.log(` ${label}: ${addr} (balance unavailable)`);
|
|
40150
|
+
return;
|
|
39950
40151
|
}
|
|
39951
|
-
|
|
39952
|
-
|
|
39953
|
-
|
|
39954
|
-
|
|
39955
|
-
|
|
40152
|
+
const flag = !bal.funded ? "\u2717 unfunded" : bal.low ? "\u26A0 low" : "\u2713";
|
|
40153
|
+
console.log(` ${label}: ${addr} ${bal.eth} ETH ${flag}`);
|
|
40154
|
+
};
|
|
40155
|
+
showBalance("owner ", ownerAddr, ownerBal);
|
|
40156
|
+
showBalance("manager", managerAddr, managerBal);
|
|
40157
|
+
if (managerBal && !managerBal.funded) {
|
|
40158
|
+
console.log(
|
|
40159
|
+
' \u2192 The manager (agent) pays gas. Fund it before "sailor run" or dispatches fail.'
|
|
40160
|
+
);
|
|
39956
40161
|
}
|
|
39957
|
-
|
|
39958
|
-
|
|
40162
|
+
if (!safe) {
|
|
40163
|
+
console.log('\nAccount: none found. Run "sailor onboard --new-sma", or pass --account <addr>.');
|
|
40164
|
+
console.log("Skipping permission checks.");
|
|
40165
|
+
return;
|
|
39959
40166
|
}
|
|
39960
|
-
|
|
39961
|
-
|
|
39962
|
-
|
|
39963
|
-
|
|
39964
|
-
|
|
39965
|
-
|
|
39966
|
-
|
|
39967
|
-
|
|
39968
|
-
|
|
39969
|
-
timeoutMs
|
|
39970
|
-
);
|
|
39971
|
-
this.walletListeners.push((addr) => {
|
|
39972
|
-
clearTimeout(timer);
|
|
39973
|
-
res(addr);
|
|
40167
|
+
console.log(`Account: ${safe}`);
|
|
40168
|
+
if (stored?.saltNonce != null) {
|
|
40169
|
+
const saltNonce = BigInt(stored.saltNonce);
|
|
40170
|
+
const MAINNET_CHAINS = [1, 8453, 42161, 130];
|
|
40171
|
+
try {
|
|
40172
|
+
const proxyCreationCode = await pc.readContract({
|
|
40173
|
+
address: SAFE_V141.proxyFactory,
|
|
40174
|
+
abi: safeProxyFactoryAbi,
|
|
40175
|
+
functionName: "proxyCreationCode"
|
|
39974
40176
|
});
|
|
39975
|
-
|
|
39976
|
-
|
|
39977
|
-
|
|
39978
|
-
|
|
39979
|
-
|
|
39980
|
-
|
|
39981
|
-
*/
|
|
39982
|
-
enqueue(req, timeoutMs = 10 * 60 * 1e3) {
|
|
39983
|
-
const id = `req_${Date.now()}_${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
|
|
39984
|
-
const request = { ...req, id, createdAt: Date.now() };
|
|
39985
|
-
const timer = setTimeout(() => {
|
|
39986
|
-
if (this.pending.has(id)) {
|
|
39987
|
-
this.pending.delete(id);
|
|
39988
|
-
this.recordResult(
|
|
39989
|
-
{
|
|
39990
|
-
status: "rejected",
|
|
39991
|
-
requestId: id,
|
|
39992
|
-
reason: `timed out after ${timeoutMs / 1e3}s`
|
|
39993
|
-
},
|
|
39994
|
-
request
|
|
40177
|
+
const ownerAddr2 = stored.owner ? getAddress(stored.owner) : null;
|
|
40178
|
+
const managerAddr2 = stored.manager ? getAddress(stored.manager) : null;
|
|
40179
|
+
if (ownerAddr2 && managerAddr2) {
|
|
40180
|
+
console.log(
|
|
40181
|
+
`
|
|
40182
|
+
Multi-chain addresses (salt ${saltNonce}, owner ${ownerAddr2}, manager ${managerAddr2}):`
|
|
39995
40183
|
);
|
|
40184
|
+
const CHAIN_NAMES = { 1: "Ethereum", 8453: "Base", 42161: "Arbitrum", 130: "Unichain" };
|
|
40185
|
+
const deployedChains = /* @__PURE__ */ new Set([
|
|
40186
|
+
stored.chainId,
|
|
40187
|
+
...stored.deployedChains ?? []
|
|
40188
|
+
]);
|
|
40189
|
+
const predictions = [];
|
|
40190
|
+
for (const cid of MAINNET_CHAINS) {
|
|
40191
|
+
const dep = sailDeployments[cid];
|
|
40192
|
+
const { predicted } = buildSmaAddressPrediction(
|
|
40193
|
+
dep,
|
|
40194
|
+
ownerAddr2,
|
|
40195
|
+
managerAddr2,
|
|
40196
|
+
saltNonce,
|
|
40197
|
+
proxyCreationCode
|
|
40198
|
+
);
|
|
40199
|
+
predictions.push(predicted.toLowerCase());
|
|
40200
|
+
const isPrimary = cid === stored.chainId && predicted.toLowerCase() === stored.safe.toLowerCase() && (safe == null || safe.toLowerCase() === stored.safe.toLowerCase());
|
|
40201
|
+
const isRecorded = deployedChains.has(cid);
|
|
40202
|
+
const label = isPrimary ? "deployed (this account)" : isRecorded ? `${predicted} \u2713 deployed (recorded)` : predicted;
|
|
40203
|
+
console.log(` ${CHAIN_NAMES[cid].padEnd(12)} (${cid}): ${label}`);
|
|
40204
|
+
}
|
|
40205
|
+
if (new Set(predictions).size === 1) {
|
|
40206
|
+
console.log(" \u2713 Same address on all chains \u2014 cross-chain SMA deployment is live.");
|
|
40207
|
+
} else {
|
|
40208
|
+
console.log(' \u26A0 Addresses differ per chain. Run "sailor account predict" for details.');
|
|
40209
|
+
}
|
|
39996
40210
|
}
|
|
39997
|
-
}
|
|
39998
|
-
|
|
39999
|
-
|
|
40000
|
-
|
|
40211
|
+
} catch {
|
|
40212
|
+
}
|
|
40213
|
+
} else if (stored) {
|
|
40214
|
+
console.log(
|
|
40215
|
+
"\nMulti-chain addresses: saltNonce not stored (deployed before salt tracking)."
|
|
40216
|
+
);
|
|
40217
|
+
console.log(" To enable: re-deploy with sailor onboard --new-sma --salt 0");
|
|
40001
40218
|
}
|
|
40002
|
-
|
|
40003
|
-
|
|
40004
|
-
|
|
40005
|
-
|
|
40006
|
-
|
|
40007
|
-
|
|
40008
|
-
if (existing) return Promise.resolve(existing);
|
|
40009
|
-
return new Promise((res) => {
|
|
40010
|
-
const timer = setTimeout(() => {
|
|
40011
|
-
this.resultWaiters.get(id)?.delete(waiter);
|
|
40012
|
-
res(null);
|
|
40013
|
-
}, timeoutMs);
|
|
40014
|
-
const waiter = (r) => {
|
|
40015
|
-
clearTimeout(timer);
|
|
40016
|
-
res(r);
|
|
40017
|
-
};
|
|
40018
|
-
if (!this.resultWaiters.has(id)) this.resultWaiters.set(id, /* @__PURE__ */ new Set());
|
|
40019
|
-
this.resultWaiters.get(id)?.add(waiter);
|
|
40020
|
-
});
|
|
40219
|
+
if (permissions.length === 0) {
|
|
40220
|
+
console.log(
|
|
40221
|
+
"\n\u26A0 No permissions registered \u2014 every dispatch will be denied (NoPermissionsRegistered)."
|
|
40222
|
+
);
|
|
40223
|
+
console.log(' Register at least one with "sailor mandate attach".');
|
|
40224
|
+
return;
|
|
40021
40225
|
}
|
|
40022
|
-
|
|
40023
|
-
|
|
40024
|
-
|
|
40025
|
-
|
|
40026
|
-
|
|
40027
|
-
|
|
40226
|
+
console.log(`
|
|
40227
|
+
Registered permissions (${permissions.length}):`);
|
|
40228
|
+
if (permsNoCode.length > 0) {
|
|
40229
|
+
console.log(
|
|
40230
|
+
`
|
|
40231
|
+
\u26A0 ${permsNoCode.length} registered permission(s) have NO contract code on chain ${chainId} \u2014 dispatches naming them will fail. Verify the address (wrong chain?) or revoke:`
|
|
40232
|
+
);
|
|
40233
|
+
permsNoCode.forEach((p) => console.log(` ${p}`));
|
|
40234
|
+
}
|
|
40235
|
+
if (caps.dispatchModel === "selective") {
|
|
40236
|
+
permissions.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
|
|
40237
|
+
console.log("\nEach dispatch names one permission, so pass-through is not required.");
|
|
40238
|
+
return;
|
|
40239
|
+
}
|
|
40240
|
+
for (let i = 0; i < checks.length; i++) {
|
|
40241
|
+
const c = checks[i];
|
|
40242
|
+
const mark = c.passesThrough ? "\u2713 pass-through" : "\u2717 NOT pass-through";
|
|
40243
|
+
console.log(` ${i + 1}. ${c.permission} ${mark}${c.note ? ` (${c.note})` : ""}`);
|
|
40244
|
+
}
|
|
40245
|
+
if (multiBricking) {
|
|
40246
|
+
console.log(
|
|
40247
|
+
`
|
|
40248
|
+
\u2717 ${bricking.length} permission(s) return false for unrelated calls. On this kernel EVERY registered permission must approve EVERY call, so these BRICK all dispatches (they surface as PermissionDenied). Revoke or replace them with pass-through versions:`
|
|
40249
|
+
);
|
|
40250
|
+
bricking.forEach((c) => console.log(` ${c.permission}`));
|
|
40251
|
+
} else if (permissions.length > 1) {
|
|
40252
|
+
console.log("\n\u2713 All permissions pass through unrelated calls \u2014 dispatch will not be bricked.");
|
|
40253
|
+
}
|
|
40254
|
+
console.log(`
|
|
40255
|
+
Probe is heuristic: an unknown selector (${PROBE_SELECTOR}) to a neutral target (${PROBE_TARGET}).`);
|
|
40256
|
+
}
|
|
40257
|
+
|
|
40258
|
+
// src/commands/init.ts
|
|
40259
|
+
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
|
40260
|
+
var import_node_path7 = __toESM(require("node:path"), 1);
|
|
40261
|
+
|
|
40262
|
+
// src/lib/foundry.ts
|
|
40263
|
+
var import_node_fs7 = require("node:fs");
|
|
40264
|
+
var import_node_path6 = require("node:path");
|
|
40265
|
+
var FOUNDRY_TOML = `[profile.default]
|
|
40266
|
+
src = "mandates"
|
|
40267
|
+
out = "out"
|
|
40268
|
+
libs = ["lib"]
|
|
40269
|
+
remappings = ["@sail/=.sail/contracts/"]
|
|
40270
|
+
solc = "0.8.26"
|
|
40271
|
+
optimizer = true
|
|
40272
|
+
optimizer_runs = 200
|
|
40273
|
+
# Mandates are deployed as standalone contracts and configured via their
|
|
40274
|
+
# constructor, then attached to a Safe with \`sailor mandate attach\`.
|
|
40275
|
+
`;
|
|
40276
|
+
var IPERMISSION_SOL = `// SPDX-License-Identifier: MIT
|
|
40277
|
+
pragma solidity 0.8.26;
|
|
40278
|
+
|
|
40279
|
+
/// @notice Execution context passed to every permission on each dispatch call.
|
|
40280
|
+
/// @dev Read-only snapshot of the transaction environment (staticcall).
|
|
40281
|
+
struct Context {
|
|
40282
|
+
address account; // the Safe whose assets are being moved
|
|
40283
|
+
address manager; // the delegated signer who submitted the dispatch
|
|
40284
|
+
address submitter; // msg.sender of the dispatch (may be a relayer)
|
|
40285
|
+
address target; // the call target
|
|
40286
|
+
bytes4 selector; // leading 4 bytes of calldata
|
|
40287
|
+
uint256 value; // native ETH forwarded (wei)
|
|
40288
|
+
uint256 blockTimestamp; // block.timestamp at dispatch
|
|
40289
|
+
uint256 blockNumber; // block.number at dispatch
|
|
40290
|
+
}
|
|
40291
|
+
|
|
40292
|
+
/// @title IPermission
|
|
40293
|
+
/// @notice Interface every Sail permission (mandate) contract must implement.
|
|
40294
|
+
/// @dev Evaluated via staticcall with a fixed gas cap; a revert or gas
|
|
40295
|
+
/// exhaustion is treated as \`false\`. Must not mutate state.
|
|
40296
|
+
interface IPermission {
|
|
40297
|
+
/// @notice Decide whether a manager-submitted transaction is permitted.
|
|
40298
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
|
|
40299
|
+
|
|
40300
|
+
/// @notice Optional stable identifier for off-chain indexing/deduplication.
|
|
40301
|
+
function discriminator() external view returns (bytes32);
|
|
40302
|
+
}
|
|
40303
|
+
`;
|
|
40304
|
+
var EXAMPLE_MANDATE_SOL = `// SPDX-License-Identifier: MIT
|
|
40305
|
+
pragma solidity 0.8.26;
|
|
40306
|
+
|
|
40307
|
+
import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
|
|
40308
|
+
|
|
40309
|
+
/// @title BoundedCallPermission
|
|
40310
|
+
/// @notice General-purpose IPermission primitive. Bounds the universal properties of any call:
|
|
40311
|
+
/// allowed targets, allowed selectors, and max ETH value. Protocol-agnostic.
|
|
40312
|
+
/// For calldata-parameter bounds (amount caps, recipient checks, slippage), write a
|
|
40313
|
+
/// protocol-specific permission \u2014 see examples/permissions/ for the pattern per protocol.
|
|
40314
|
+
/// @dev Deploy one instance per SMA with constructor-configured parameters.
|
|
40315
|
+
contract BoundedCallPermission is IPermission {
|
|
40316
|
+
bytes32 private constant DISCRIMINATOR = keccak256("BoundedCallPermission");
|
|
40317
|
+
|
|
40318
|
+
mapping(address => bool) public isAllowedTarget;
|
|
40319
|
+
mapping(bytes4 => bool) public isAllowedSelector;
|
|
40320
|
+
bool public immutable SELECTOR_FILTERING;
|
|
40321
|
+
uint256 public immutable MAX_VALUE;
|
|
40322
|
+
|
|
40323
|
+
constructor(address[] memory allowedTargets, bytes4[] memory allowedSelectors, uint256 maxValue) {
|
|
40324
|
+
for (uint256 i = 0; i < allowedTargets.length; i++) isAllowedTarget[allowedTargets[i]] = true;
|
|
40325
|
+
SELECTOR_FILTERING = allowedSelectors.length > 0;
|
|
40326
|
+
for (uint256 i = 0; i < allowedSelectors.length; i++) isAllowedSelector[allowedSelectors[i]] = true;
|
|
40327
|
+
MAX_VALUE = maxValue;
|
|
40028
40328
|
}
|
|
40029
|
-
|
|
40030
|
-
|
|
40031
|
-
|
|
40032
|
-
|
|
40033
|
-
|
|
40034
|
-
|
|
40035
|
-
const waiters2 = this.resultWaiters.get(id);
|
|
40036
|
-
if (waiters2) {
|
|
40037
|
-
for (const w of waiters2) w(response);
|
|
40038
|
-
this.resultWaiters.delete(id);
|
|
40329
|
+
|
|
40330
|
+
function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) {
|
|
40331
|
+
if (!isAllowedTarget[ctx.target]) return false;
|
|
40332
|
+
if (SELECTOR_FILTERING && !isAllowedSelector[ctx.selector]) return false;
|
|
40333
|
+
if (ctx.value > MAX_VALUE) return false;
|
|
40334
|
+
return true;
|
|
40039
40335
|
}
|
|
40040
|
-
|
|
40041
|
-
|
|
40042
|
-
|
|
40336
|
+
|
|
40337
|
+
function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
|
|
40338
|
+
}
|
|
40339
|
+
`;
|
|
40340
|
+
var MANDATES_README = `# Mandates
|
|
40341
|
+
|
|
40342
|
+
Solidity permission contracts for this Sailor project live here.
|
|
40343
|
+
|
|
40344
|
+
A permission implements \`@sail/interfaces/IPermission.sol\` \u2014 \`evaluate(txData, ctx)\`
|
|
40345
|
+
returns \`true\` to permit a manager-submitted dispatch, \`false\` to block it.
|
|
40346
|
+
|
|
40347
|
+
## Authoring + deploying
|
|
40348
|
+
|
|
40349
|
+
1. Start from \`BoundedCallPermission.sol\` for target/selector/value gating.
|
|
40350
|
+
For calldata-parameter bounds (amount caps, slippage, recipient checks),
|
|
40351
|
+
decode \`txData\` with the target protocol's ABI and add bounds to \`evaluate()\`.
|
|
40352
|
+
Configure all parameters in the **constructor** \u2014 the deploy flow expects a
|
|
40353
|
+
single creation transaction to fully set up the permission.
|
|
40354
|
+
2. Compile:
|
|
40355
|
+
\`\`\`bash
|
|
40356
|
+
forge build
|
|
40357
|
+
\`\`\`
|
|
40358
|
+
3. Deploy it (the owner signs the creation tx in the browser signing UI):
|
|
40359
|
+
\`\`\`bash
|
|
40360
|
+
sailor mandate deploy --contract BoundedCallPermission \\
|
|
40361
|
+
--args '[["0xTarget1", "0xTarget2"], [], 0]'
|
|
40362
|
+
\`\`\`
|
|
40363
|
+
Args: (allowedTargets[], allowedSelectors[], maxValue).
|
|
40364
|
+
Pass an empty selector array [] to skip selector filtering.
|
|
40365
|
+
Pass 0 for maxValue to block all ETH transfers.
|
|
40366
|
+
4. Attach it to a Safe:
|
|
40367
|
+
\`\`\`bash
|
|
40368
|
+
sailor mandate attach --address 0xDeployed --sma 0xSafe
|
|
40369
|
+
\`\`\`
|
|
40370
|
+
(or pass \`--attach --sma 0xSafe\` to \`deploy\` to do both at once.)
|
|
40371
|
+
|
|
40372
|
+
Compiled artifacts are written to \`out/\` and the deployed address is tracked in
|
|
40373
|
+
\`.sail/state/mandates.json\`.
|
|
40374
|
+
`;
|
|
40375
|
+
function scaffoldFoundryWorkspace(root) {
|
|
40376
|
+
const dirs = [(0, import_node_path6.join)(root, "mandates"), (0, import_node_path6.join)(root, ".sail", "contracts", "interfaces")];
|
|
40377
|
+
for (const d of dirs) {
|
|
40378
|
+
if (!(0, import_node_fs7.existsSync)(d)) (0, import_node_fs7.mkdirSync)(d, { recursive: true });
|
|
40043
40379
|
}
|
|
40044
|
-
|
|
40045
|
-
|
|
40046
|
-
|
|
40047
|
-
|
|
40048
|
-
|
|
40049
|
-
|
|
40050
|
-
|
|
40051
|
-
|
|
40052
|
-
|
|
40053
|
-
|
|
40054
|
-
|
|
40055
|
-
|
|
40056
|
-
|
|
40057
|
-
|
|
40058
|
-
|
|
40059
|
-
|
|
40060
|
-
|
|
40061
|
-
|
|
40062
|
-
|
|
40063
|
-
|
|
40064
|
-
|
|
40065
|
-
|
|
40066
|
-
|
|
40380
|
+
writeIfMissing((0, import_node_path6.join)(root, "foundry.toml"), FOUNDRY_TOML);
|
|
40381
|
+
writeIfMissing(
|
|
40382
|
+
(0, import_node_path6.join)(root, ".sail", "contracts", "interfaces", "IPermission.sol"),
|
|
40383
|
+
IPERMISSION_SOL
|
|
40384
|
+
);
|
|
40385
|
+
writeIfMissing((0, import_node_path6.join)(root, "mandates", "BoundedCallPermission.sol"), EXAMPLE_MANDATE_SOL);
|
|
40386
|
+
writeIfMissing((0, import_node_path6.join)(root, "mandates", "README.md"), MANDATES_README);
|
|
40387
|
+
}
|
|
40388
|
+
function writeIfMissing(path8, content) {
|
|
40389
|
+
if (!(0, import_node_fs7.existsSync)(path8)) (0, import_node_fs7.writeFileSync)(path8, content, "utf8");
|
|
40390
|
+
}
|
|
40391
|
+
|
|
40392
|
+
// src/commands/init.ts
|
|
40393
|
+
var TEMPLATE_COPY_EXCLUDES = /* @__PURE__ */ new Set([
|
|
40394
|
+
"node_modules",
|
|
40395
|
+
"dist",
|
|
40396
|
+
"out",
|
|
40397
|
+
"cache",
|
|
40398
|
+
"broadcast",
|
|
40399
|
+
".git"
|
|
40400
|
+
]);
|
|
40401
|
+
function copyDirSync(src, dest) {
|
|
40402
|
+
import_node_fs8.default.mkdirSync(dest, { recursive: true });
|
|
40403
|
+
for (const entry of import_node_fs8.default.readdirSync(src, { withFileTypes: true })) {
|
|
40404
|
+
if (TEMPLATE_COPY_EXCLUDES.has(entry.name)) continue;
|
|
40405
|
+
const srcPath = import_node_path7.default.join(src, entry.name);
|
|
40406
|
+
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
40407
|
+
const destPath = import_node_path7.default.join(dest, destName);
|
|
40408
|
+
if (entry.isDirectory()) {
|
|
40409
|
+
copyDirSync(srcPath, destPath);
|
|
40067
40410
|
} else {
|
|
40068
|
-
|
|
40069
|
-
}
|
|
40070
|
-
try {
|
|
40071
|
-
appendActivity(event, (0, import_node_path7.join)(this.projectRoot, ".sail"));
|
|
40072
|
-
} catch {
|
|
40411
|
+
import_node_fs8.default.copyFileSync(srcPath, destPath);
|
|
40073
40412
|
}
|
|
40074
40413
|
}
|
|
40075
|
-
|
|
40076
|
-
|
|
40077
|
-
|
|
40414
|
+
}
|
|
40415
|
+
var SAIL_WORKSPACE_README = `# Sailor Project Workspace
|
|
40416
|
+
|
|
40417
|
+
This folder is the local workspace for one Sailor agent deployment.
|
|
40418
|
+
|
|
40419
|
+
## Layout
|
|
40420
|
+
|
|
40421
|
+
- \`config.json\` is the project manifest: name, chain, and state location.
|
|
40422
|
+
- \`keys/\` stores encrypted local signing keys. Never commit these files.
|
|
40423
|
+
- \`runtime/\` is for local UI and signing handoff state.
|
|
40424
|
+
- \`state/\` is for persistent agent state, audit logs, and tx history.
|
|
40425
|
+
|
|
40426
|
+
AI coding agents should read the project's \`AGENTS.md\` and this folder's \`config.json\`
|
|
40427
|
+
before changing strategy code or running commands that touch funds.
|
|
40428
|
+
`;
|
|
40429
|
+
function writeIfMissing2(file, content) {
|
|
40430
|
+
if (!import_node_fs8.default.existsSync(file)) import_node_fs8.default.writeFileSync(file, content, "utf-8");
|
|
40431
|
+
}
|
|
40432
|
+
function scaffoldProjectWorkspace(dest, name, options) {
|
|
40433
|
+
const chainId = options.chain ? (() => {
|
|
40434
|
+
const n = Number(options.chain);
|
|
40435
|
+
if (!Number.isInteger(n) || n <= 0) throw new Error(`Invalid chain id: "${options.chain}"`);
|
|
40436
|
+
return n;
|
|
40437
|
+
})() : null;
|
|
40438
|
+
const sailDir2 = import_node_path7.default.join(dest, ".sail");
|
|
40439
|
+
import_node_fs8.default.mkdirSync(import_node_path7.default.join(sailDir2, "keys"), { recursive: true });
|
|
40440
|
+
import_node_fs8.default.mkdirSync(import_node_path7.default.join(sailDir2, "runtime"), { recursive: true });
|
|
40441
|
+
import_node_fs8.default.mkdirSync(import_node_path7.default.join(sailDir2, "state"), { recursive: true });
|
|
40442
|
+
import_node_fs8.default.writeFileSync(
|
|
40443
|
+
import_node_path7.default.join(sailDir2, "config.json"),
|
|
40444
|
+
`${JSON.stringify(
|
|
40445
|
+
{
|
|
40446
|
+
version: 1,
|
|
40447
|
+
name,
|
|
40448
|
+
chainId,
|
|
40449
|
+
// null = chain not yet chosen; Stage 1 will set this
|
|
40450
|
+
stateDir: ".sail/state",
|
|
40451
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
40452
|
+
contracts: {
|
|
40453
|
+
kernel: "",
|
|
40454
|
+
mandateFactory: ""
|
|
40455
|
+
}
|
|
40456
|
+
},
|
|
40457
|
+
null,
|
|
40458
|
+
2
|
|
40459
|
+
)}
|
|
40460
|
+
`,
|
|
40461
|
+
"utf-8"
|
|
40462
|
+
);
|
|
40463
|
+
writeIfMissing2(import_node_path7.default.join(sailDir2, "README.md"), SAIL_WORKSPACE_README);
|
|
40464
|
+
const chainIdLine = chainId != null ? `CHAIN_ID=${chainId}
|
|
40465
|
+
` : `# CHAIN_ID=8453 # set after choosing your chain in Stage 1
|
|
40466
|
+
`;
|
|
40467
|
+
import_node_fs8.default.writeFileSync(
|
|
40468
|
+
import_node_path7.default.join(dest, ".env.example"),
|
|
40469
|
+
`# Sailor agent environment
|
|
40470
|
+
RPC_URL=https://your-rpc-endpoint
|
|
40471
|
+
${chainIdLine}
|
|
40472
|
+
# Optional for non-interactive runs
|
|
40473
|
+
# SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
|
|
40474
|
+
`,
|
|
40475
|
+
"utf-8"
|
|
40476
|
+
);
|
|
40477
|
+
const rpcLine = options.rpcUrl ? `RPC_URL=${options.rpcUrl}` : `# Paste your RPC endpoint here (Alchemy, Infura, or any HTTPS endpoint)
|
|
40478
|
+
# RPC_URL=https://your-rpc-endpoint`;
|
|
40479
|
+
const chainLine = chainId != null ? `
|
|
40480
|
+
CHAIN_ID=${chainId}` : ``;
|
|
40481
|
+
writeIfMissing2(
|
|
40482
|
+
import_node_path7.default.join(sailDir2, ".env.local"),
|
|
40483
|
+
`${rpcLine}${chainLine}
|
|
40484
|
+
|
|
40485
|
+
# Optional for non-interactive runs (CI, GitHub Actions, launchd, systemd)
|
|
40486
|
+
# SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
|
|
40487
|
+
`
|
|
40488
|
+
);
|
|
40489
|
+
}
|
|
40490
|
+
async function initCommand(dir, options = {}) {
|
|
40491
|
+
const inPlace = !dir || dir === ".";
|
|
40492
|
+
const dest = inPlace ? process.cwd() : import_node_path7.default.resolve(process.cwd(), dir);
|
|
40493
|
+
const name = import_node_path7.default.basename(dest);
|
|
40494
|
+
const templatesDir = import_node_path7.default.join(packageRoot(), "templates");
|
|
40495
|
+
const templateName = options.template ?? "default";
|
|
40496
|
+
if (/[/\\.]/.test(templateName) || templateName.includes("..")) {
|
|
40497
|
+
throw new Error(`Invalid template name: "${templateName}"`);
|
|
40078
40498
|
}
|
|
40079
|
-
|
|
40080
|
-
|
|
40081
|
-
|
|
40082
|
-
|
|
40083
|
-
|
|
40084
|
-
|
|
40085
|
-
|
|
40086
|
-
|
|
40087
|
-
|
|
40088
|
-
|
|
40089
|
-
}
|
|
40499
|
+
const templateSrc = import_node_path7.default.join(templatesDir, templateName);
|
|
40500
|
+
const availableTemplates = () => import_node_fs8.default.existsSync(templatesDir) ? import_node_fs8.default.readdirSync(templatesDir).filter((e) => import_node_fs8.default.existsSync(import_node_path7.default.join(templatesDir, e, "package.json"))).join(", ") || "none" : "none";
|
|
40501
|
+
if (!import_node_fs8.default.existsSync(templateSrc) || !import_node_fs8.default.existsSync(import_node_path7.default.join(templateSrc, "package.json"))) {
|
|
40502
|
+
const available = availableTemplates();
|
|
40503
|
+
const hint = available === "none" ? `
|
|
40504
|
+
No templates found under ${templatesDir}.
|
|
40505
|
+
If you're running the in-tree CLI bundle from a monorepo checkout, the scaffolder
|
|
40506
|
+
couldn't locate the repo's templates/ directory. Install the published package, or
|
|
40507
|
+
run from the repo root.` : ` Available: ${available}`;
|
|
40508
|
+
throw new Error(`Template "${templateName}" not found.${hint}`);
|
|
40090
40509
|
}
|
|
40091
|
-
|
|
40092
|
-
|
|
40093
|
-
|
|
40094
|
-
* `state/accounts.json` (so the account switcher and the agent see it) BEFORE
|
|
40095
|
-
* overwriting `account.json` with the new active SMA — the upsert backfills
|
|
40096
|
-
* from the previously-active account.json, so writing it first would drop the
|
|
40097
|
-
* prior SMA.
|
|
40098
|
-
*/
|
|
40099
|
-
handleSaveAccount(req, res) {
|
|
40100
|
-
this.readBody(req).then((body) => {
|
|
40101
|
-
const parsed = body ? JSON.parse(body) : {};
|
|
40102
|
-
const { safe, owner: owner2, permissionSigner, manager, chainId, createdAtBlock } = parsed;
|
|
40103
|
-
if (!safe || !owner2 || !chainId) {
|
|
40104
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
40105
|
-
res.end(JSON.stringify({ error: "safe, owner, and chainId are required" }));
|
|
40106
|
-
return;
|
|
40107
|
-
}
|
|
40108
|
-
const record = {
|
|
40109
|
-
safe,
|
|
40110
|
-
owner: owner2,
|
|
40111
|
-
permissionSigner: permissionSigner ?? owner2,
|
|
40112
|
-
manager: manager ?? owner2,
|
|
40113
|
-
chainId,
|
|
40114
|
-
createdAtBlock: createdAtBlock ?? "0"
|
|
40115
|
-
};
|
|
40116
|
-
const baseSailDir = this.sailFile();
|
|
40117
|
-
upsertAccountInList(record, void 0, baseSailDir);
|
|
40118
|
-
(0, import_node_fs8.mkdirSync)(baseSailDir, { recursive: true });
|
|
40119
|
-
(0, import_node_fs8.writeFileSync)(this.sailFile("account.json"), `${JSON.stringify(record, null, 2)}
|
|
40120
|
-
`);
|
|
40121
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40122
|
-
res.end(JSON.stringify({ ok: true }));
|
|
40123
|
-
}).catch((err) => {
|
|
40124
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
40125
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
40126
|
-
});
|
|
40510
|
+
const cwd = process.cwd();
|
|
40511
|
+
if (!inPlace && !dest.startsWith(cwd + import_node_path7.default.sep) && dest !== cwd) {
|
|
40512
|
+
throw new Error(`Directory must be inside the current working directory`);
|
|
40127
40513
|
}
|
|
40128
|
-
|
|
40129
|
-
|
|
40130
|
-
|
|
40131
|
-
|
|
40132
|
-
|
|
40133
|
-
|
|
40134
|
-
|
|
40135
|
-
|
|
40136
|
-
|
|
40137
|
-
|
|
40138
|
-
|
|
40139
|
-
|
|
40140
|
-
|
|
40141
|
-
|
|
40142
|
-
|
|
40143
|
-
|
|
40144
|
-
|
|
40145
|
-
|
|
40146
|
-
|
|
40514
|
+
if (!inPlace && import_node_fs8.default.existsSync(dest)) {
|
|
40515
|
+
throw new Error(`Directory already exists: ${dest}`);
|
|
40516
|
+
}
|
|
40517
|
+
if (inPlace && import_node_fs8.default.existsSync(import_node_path7.default.join(dest, ".sail", "config.json"))) {
|
|
40518
|
+
throw new Error(`Already initialized \u2014 .sail/config.json exists`);
|
|
40519
|
+
}
|
|
40520
|
+
copyDirSync(templateSrc, dest);
|
|
40521
|
+
const pkgRoot = packageRoot();
|
|
40522
|
+
const examplesPermSrc = import_node_path7.default.join(pkgRoot, "examples", "permissions");
|
|
40523
|
+
if (import_node_fs8.default.existsSync(examplesPermSrc)) {
|
|
40524
|
+
copyDirSync(examplesPermSrc, import_node_path7.default.join(dest, "examples", "permissions"));
|
|
40525
|
+
}
|
|
40526
|
+
const permModelSrc = import_node_path7.default.join(pkgRoot, "docs", "PERMISSION_MODEL.md");
|
|
40527
|
+
if (import_node_fs8.default.existsSync(permModelSrc)) {
|
|
40528
|
+
import_node_fs8.default.mkdirSync(import_node_path7.default.join(dest, "docs"), { recursive: true });
|
|
40529
|
+
writeIfMissing2(import_node_path7.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs8.default.readFileSync(permModelSrc, "utf-8"));
|
|
40530
|
+
}
|
|
40531
|
+
const pkgPath = import_node_path7.default.join(dest, "package.json");
|
|
40532
|
+
if (import_node_fs8.default.existsSync(pkgPath)) {
|
|
40533
|
+
const pkg = JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8"));
|
|
40534
|
+
pkg.name = name;
|
|
40535
|
+
const deps = pkg.dependencies ?? {};
|
|
40536
|
+
if (deps["@sail/sdk"] === "workspace:*") {
|
|
40537
|
+
const sdkPath = import_node_path7.default.join(pkgRoot, "packages", "sdk");
|
|
40538
|
+
deps["@sail/sdk"] = import_node_fs8.default.existsSync(sdkPath) ? `file:${sdkPath}` : (
|
|
40539
|
+
// Fallback: SDK not bundled — user must install it manually.
|
|
40540
|
+
"0.1.0"
|
|
40147
40541
|
);
|
|
40148
|
-
} catch {
|
|
40149
|
-
try {
|
|
40150
|
-
const a = JSON.parse(
|
|
40151
|
-
(0, import_node_fs8.readFileSync)(this.sailFile("account.json"), "utf-8")
|
|
40152
|
-
);
|
|
40153
|
-
res.end(JSON.stringify([{ ...a, name: "My SMA", active: true, addedAt: null }]));
|
|
40154
|
-
} catch {
|
|
40155
|
-
res.end("[]");
|
|
40156
|
-
}
|
|
40157
40542
|
}
|
|
40543
|
+
pkg.dependencies = deps;
|
|
40544
|
+
import_node_fs8.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
40545
|
+
`);
|
|
40158
40546
|
}
|
|
40159
|
-
|
|
40160
|
-
|
|
40161
|
-
|
|
40162
|
-
|
|
40163
|
-
|
|
40164
|
-
|
|
40165
|
-
|
|
40166
|
-
|
|
40167
|
-
|
|
40168
|
-
|
|
40169
|
-
|
|
40170
|
-
|
|
40171
|
-
|
|
40547
|
+
scaffoldProjectWorkspace(dest, name, options);
|
|
40548
|
+
scaffoldFoundryWorkspace(dest);
|
|
40549
|
+
printWelcome(
|
|
40550
|
+
dest,
|
|
40551
|
+
name,
|
|
40552
|
+
inPlace,
|
|
40553
|
+
!!options.rpcUrl,
|
|
40554
|
+
/* freshInit */
|
|
40555
|
+
true
|
|
40556
|
+
);
|
|
40557
|
+
}
|
|
40558
|
+
function chainLabel(chainId) {
|
|
40559
|
+
const labels = {
|
|
40560
|
+
8453: "Base",
|
|
40561
|
+
42161: "Arbitrum",
|
|
40562
|
+
84532: "Base Sepolia",
|
|
40563
|
+
130: "Unichain"
|
|
40564
|
+
};
|
|
40565
|
+
return labels[chainId] ?? `Chain ${chainId}`;
|
|
40566
|
+
}
|
|
40567
|
+
function detectState(dest) {
|
|
40568
|
+
try {
|
|
40569
|
+
const configRaw = import_node_fs8.default.readFileSync(import_node_path7.default.join(dest, ".sail", "config.json"), "utf-8");
|
|
40570
|
+
const config = JSON.parse(configRaw);
|
|
40571
|
+
const projectName = config.name ?? import_node_path7.default.basename(dest);
|
|
40572
|
+
const accountPath = import_node_path7.default.join(dest, ".sail", "account.json");
|
|
40573
|
+
if (!import_node_fs8.default.existsSync(accountPath)) {
|
|
40574
|
+
return { kind: "B", projectName, chain: chainLabel(config.chainId ?? 0) };
|
|
40172
40575
|
}
|
|
40173
|
-
const
|
|
40174
|
-
|
|
40175
|
-
|
|
40176
|
-
|
|
40177
|
-
|
|
40178
|
-
|
|
40179
|
-
|
|
40180
|
-
|
|
40181
|
-
|
|
40182
|
-
port: this.port,
|
|
40183
|
-
pid: process.pid,
|
|
40184
|
-
pendingCount: this.pending.size
|
|
40185
|
-
})
|
|
40576
|
+
const accountRaw = import_node_fs8.default.readFileSync(accountPath, "utf-8");
|
|
40577
|
+
const account2 = JSON.parse(accountRaw);
|
|
40578
|
+
const sma = account2.safe ?? "";
|
|
40579
|
+
const chain2 = chainLabel(account2.chainId ?? config.chainId ?? 0);
|
|
40580
|
+
let permissionCount = 0;
|
|
40581
|
+
try {
|
|
40582
|
+
const mandatesRaw = import_node_fs8.default.readFileSync(
|
|
40583
|
+
import_node_path7.default.join(dest, ".sail", "state", "mandates.json"),
|
|
40584
|
+
"utf-8"
|
|
40186
40585
|
);
|
|
40187
|
-
|
|
40188
|
-
|
|
40189
|
-
|
|
40190
|
-
const isAuthenticated = secretHeader === this.requestSecret;
|
|
40191
|
-
if (url === "/pending") {
|
|
40192
|
-
if (!isAuthenticated) {
|
|
40193
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
40194
|
-
res.end(JSON.stringify({ error: "forbidden" }));
|
|
40195
|
-
return;
|
|
40196
|
-
}
|
|
40197
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40198
|
-
res.end(JSON.stringify(Array.from(this.pending.values()).map((e) => e.request)));
|
|
40199
|
-
return;
|
|
40200
|
-
}
|
|
40201
|
-
if (url === "/wallet") {
|
|
40202
|
-
if (!isAuthenticated) {
|
|
40203
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
40204
|
-
res.end(JSON.stringify({ error: "forbidden" }));
|
|
40205
|
-
return;
|
|
40206
|
-
}
|
|
40207
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40208
|
-
res.end(JSON.stringify({ address: this._connectedWallet ?? null }));
|
|
40209
|
-
return;
|
|
40210
|
-
}
|
|
40211
|
-
if (url === "/requests" && req.method === "POST") {
|
|
40212
|
-
const supplied = req.headers[REQUEST_SECRET_HEADER];
|
|
40213
|
-
if (supplied !== this.requestSecret) {
|
|
40214
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
40215
|
-
res.end(JSON.stringify({ error: "forbidden" }));
|
|
40216
|
-
return;
|
|
40217
|
-
}
|
|
40218
|
-
this.readBody(req).then((body) => {
|
|
40219
|
-
const parsed = JSON.parse(body);
|
|
40220
|
-
if (!parsed.kind || !["create-sma", "deploy-mandate", "register-permission", "attach-mandate", "revoke-permissions", "set-delegate", "arbitrary-tx"].includes(parsed.kind)) {
|
|
40221
|
-
throw new Error(`Unknown signing request kind: ${String(parsed.kind)}`);
|
|
40222
|
-
}
|
|
40223
|
-
const request = this.enqueue(parsed);
|
|
40224
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40225
|
-
res.end(JSON.stringify({ id: request.id }));
|
|
40226
|
-
}).catch((err) => {
|
|
40227
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
40228
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
40229
|
-
});
|
|
40230
|
-
return;
|
|
40231
|
-
}
|
|
40232
|
-
if (url === "/api/account" && req.method === "POST") {
|
|
40233
|
-
this.handleSaveAccount(req, res);
|
|
40234
|
-
return;
|
|
40235
|
-
}
|
|
40236
|
-
if (url === "/api/account" && (req.method === "GET" || req.method == null)) {
|
|
40237
|
-
this.sendJsonFile(res, (0, import_node_path7.join)(this.projectRoot, ".sail", "account.json"), {
|
|
40238
|
-
status: 404,
|
|
40239
|
-
body: { error: "account not found" }
|
|
40240
|
-
});
|
|
40241
|
-
return;
|
|
40586
|
+
const mandates = JSON.parse(mandatesRaw);
|
|
40587
|
+
permissionCount = Array.isArray(mandates.mandates) ? mandates.mandates.length : 0;
|
|
40588
|
+
} catch {
|
|
40242
40589
|
}
|
|
40243
|
-
if (
|
|
40244
|
-
|
|
40245
|
-
return;
|
|
40590
|
+
if (permissionCount > 0) {
|
|
40591
|
+
return { kind: "D", projectName, chain: chain2, sma, permissionCount };
|
|
40246
40592
|
}
|
|
40247
|
-
|
|
40248
|
-
|
|
40249
|
-
|
|
40250
|
-
|
|
40251
|
-
|
|
40252
|
-
|
|
40253
|
-
|
|
40254
|
-
|
|
40255
|
-
|
|
40256
|
-
|
|
40257
|
-
|
|
40258
|
-
|
|
40259
|
-
|
|
40260
|
-
|
|
40261
|
-
|
|
40262
|
-
|
|
40263
|
-
|
|
40593
|
+
return { kind: "C", projectName, chain: chain2, sma };
|
|
40594
|
+
} catch {
|
|
40595
|
+
return { kind: "A" };
|
|
40596
|
+
}
|
|
40597
|
+
}
|
|
40598
|
+
function printWelcome(dest, name, inPlace, hasRpc, freshInit = false) {
|
|
40599
|
+
const state = freshInit ? { kind: "A" } : detectState(dest);
|
|
40600
|
+
if (state.kind === "B") {
|
|
40601
|
+
console.log("\nWelcome back.\n");
|
|
40602
|
+
console.log(`Project: ${state.projectName} | Network: ${state.chain}`);
|
|
40603
|
+
console.log("Status: SMA not yet deployed.\n");
|
|
40604
|
+
console.log("Next:");
|
|
40605
|
+
console.log(" sailor ui start");
|
|
40606
|
+
console.log(" Connect your wallet and deploy your SMA in the browser.\n");
|
|
40607
|
+
console.log('Or open this folder in your AI coding assistant and say: "continue"');
|
|
40608
|
+
return;
|
|
40609
|
+
}
|
|
40610
|
+
if (state.kind === "C") {
|
|
40611
|
+
console.log("\nWelcome back.\n");
|
|
40612
|
+
console.log(`Project: ${state.projectName}`);
|
|
40613
|
+
console.log(`SMA: ${state.sma} on ${state.chain}`);
|
|
40614
|
+
console.log(
|
|
40615
|
+
"Permissions: none registered yet \u2014 your agent has no mandate to execute against.\n"
|
|
40616
|
+
);
|
|
40617
|
+
console.log("Next:");
|
|
40618
|
+
console.log(
|
|
40619
|
+
" Write your permission contract in mandates/ (start from BoundedCallPermission.sol)"
|
|
40620
|
+
);
|
|
40621
|
+
console.log(" forge build");
|
|
40622
|
+
console.log(` sailor mandate deploy --contract <Name> --attach --sma ${state.sma}
|
|
40623
|
+
`);
|
|
40624
|
+
console.log('Or open this folder in your AI coding assistant and say: "continue"');
|
|
40625
|
+
return;
|
|
40626
|
+
}
|
|
40627
|
+
if (state.kind === "D") {
|
|
40628
|
+
console.log("\nWelcome back.\n");
|
|
40629
|
+
console.log(`Project: ${state.projectName}`);
|
|
40630
|
+
console.log(`SMA: ${state.sma} on ${state.chain}`);
|
|
40631
|
+
console.log(`Permissions: ${state.permissionCount} registered
|
|
40632
|
+
`);
|
|
40633
|
+
console.log('Open this folder in your AI coding assistant and say: "continue"');
|
|
40634
|
+
return;
|
|
40635
|
+
}
|
|
40636
|
+
if (!inPlace) console.log(`
|
|
40637
|
+
Created ${name}/`);
|
|
40638
|
+
console.log("\nYour Sail agent project is ready. Open your AI coding assistant in this folder and say start.");
|
|
40639
|
+
}
|
|
40640
|
+
|
|
40641
|
+
// src/commands/keys.ts
|
|
40642
|
+
var import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
40643
|
+
var import_node_path8 = __toESM(require("node:path"), 1);
|
|
40644
|
+
async function keysGenerate() {
|
|
40645
|
+
const roleInput = await prompt("Which key? (agent wallet / mandate signer)", "agent wallet");
|
|
40646
|
+
const role = normalizeRole(roleInput);
|
|
40647
|
+
if (!role) {
|
|
40648
|
+
throw new Error(`Unknown key role: "${roleInput}". Choose "agent wallet" or "mandate signer".`);
|
|
40649
|
+
}
|
|
40650
|
+
if (fileExists(keyPath(role))) {
|
|
40651
|
+
const overwrite = await confirm(
|
|
40652
|
+
`A ${roleLabel(role)} key already exists at .sail/keys/${role}.json. Overwrite it?`
|
|
40653
|
+
);
|
|
40654
|
+
if (!overwrite) {
|
|
40655
|
+
console.log("Aborted \u2014 existing key left untouched.");
|
|
40264
40656
|
return;
|
|
40265
40657
|
}
|
|
40266
|
-
if (this.uiDist) {
|
|
40267
|
-
const rawPath = (req.url ?? "/").split("?")[0];
|
|
40268
|
-
const filePath = (0, import_node_path7.resolve)((0, import_node_path7.join)(this.uiDist, rawPath === "/" ? "index.html" : rawPath));
|
|
40269
|
-
if (!filePath.startsWith((0, import_node_path7.resolve)(this.uiDist))) {
|
|
40270
|
-
res.writeHead(403);
|
|
40271
|
-
res.end();
|
|
40272
|
-
return;
|
|
40273
|
-
}
|
|
40274
|
-
if ((0, import_node_fs8.existsSync)(filePath)) {
|
|
40275
|
-
const mime = MIME[(0, import_node_path7.extname)(filePath)] ?? "application/octet-stream";
|
|
40276
|
-
res.writeHead(200, { "Content-Type": mime });
|
|
40277
|
-
res.end((0, import_node_fs8.readFileSync)(filePath));
|
|
40278
|
-
return;
|
|
40279
|
-
}
|
|
40280
|
-
const indexHtml = (0, import_node_path7.join)(this.uiDist, "index.html");
|
|
40281
|
-
if ((0, import_node_fs8.existsSync)(indexHtml)) {
|
|
40282
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
40283
|
-
res.end((0, import_node_fs8.readFileSync)(indexHtml));
|
|
40284
|
-
return;
|
|
40285
|
-
}
|
|
40286
|
-
}
|
|
40287
|
-
res.writeHead(404);
|
|
40288
|
-
res.end();
|
|
40289
40658
|
}
|
|
40290
|
-
|
|
40291
|
-
|
|
40292
|
-
|
|
40293
|
-
|
|
40294
|
-
|
|
40295
|
-
|
|
40296
|
-
|
|
40297
|
-
|
|
40298
|
-
|
|
40299
|
-
|
|
40300
|
-
|
|
40301
|
-
|
|
40659
|
+
const password = await promptHidden("Set a password to encrypt the key");
|
|
40660
|
+
if (password.length < 8) {
|
|
40661
|
+
throw new Error("Password must be at least 8 characters.");
|
|
40662
|
+
}
|
|
40663
|
+
const confirmation = await promptHidden("Confirm password");
|
|
40664
|
+
if (password !== confirmation) {
|
|
40665
|
+
throw new Error("Passwords do not match.");
|
|
40666
|
+
}
|
|
40667
|
+
const keyring = LocalKeyring.generate();
|
|
40668
|
+
const keystore = await keyring.exportKeystore(password);
|
|
40669
|
+
writeJsonFile(keyPath(role), keystore);
|
|
40670
|
+
const label = role === "manager" ? "Agent wallet" : "Mandate signer";
|
|
40671
|
+
console.log(`
|
|
40672
|
+
${label} key saved. Address: ${checksum4(keyring.address)}`);
|
|
40673
|
+
console.log(`Encrypted keystore written to .sail/keys/${role}.json`);
|
|
40674
|
+
if (role === "manager") {
|
|
40675
|
+
const save = await confirm(
|
|
40676
|
+
"\nSave passphrase to .sail/.env.local for non-interactive use? (required for CI/GitHub Actions)"
|
|
40677
|
+
);
|
|
40678
|
+
if (save) {
|
|
40679
|
+
const envPath = sailPath(".env.local");
|
|
40680
|
+
let content = "";
|
|
40681
|
+
if (import_node_fs9.default.existsSync(envPath)) {
|
|
40682
|
+
content = import_node_fs9.default.readFileSync(envPath, "utf-8");
|
|
40683
|
+
content = content.replace(/^SAIL_PASSPHRASE=.*\n?/m, "");
|
|
40302
40684
|
}
|
|
40303
|
-
|
|
40304
|
-
|
|
40305
|
-
|
|
40306
|
-
|
|
40307
|
-
|
|
40308
|
-
|
|
40309
|
-
if (typeof msg.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(msg.address)) return;
|
|
40310
|
-
this._connectedWallet = msg.address;
|
|
40311
|
-
for (const listener of this.walletListeners) listener(msg.address);
|
|
40312
|
-
this.walletListeners = [];
|
|
40313
|
-
return;
|
|
40314
|
-
}
|
|
40315
|
-
if (msg.type === "wallet-disconnected") {
|
|
40316
|
-
this._connectedWallet = void 0;
|
|
40317
|
-
return;
|
|
40318
|
-
}
|
|
40319
|
-
const entry = this.pending.get(msg.requestId);
|
|
40320
|
-
if (!entry) return;
|
|
40321
|
-
clearTimeout(entry.timer);
|
|
40322
|
-
this.pending.delete(msg.requestId);
|
|
40323
|
-
const { request } = entry;
|
|
40324
|
-
if (msg.type === "signed") {
|
|
40325
|
-
this.recordResult(
|
|
40326
|
-
{ status: "signed", requestId: msg.requestId, txHash: msg.txHash },
|
|
40327
|
-
request
|
|
40328
|
-
);
|
|
40329
|
-
} else if (msg.type === "signature") {
|
|
40330
|
-
this.recordResult(
|
|
40331
|
-
{ status: "signature", requestId: msg.requestId, signature: msg.signature },
|
|
40332
|
-
request
|
|
40333
|
-
);
|
|
40685
|
+
content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `SAIL_PASSPHRASE=${password}
|
|
40686
|
+
`;
|
|
40687
|
+
import_node_fs9.default.mkdirSync(import_node_path8.default.dirname(envPath), { recursive: true });
|
|
40688
|
+
import_node_fs9.default.writeFileSync(envPath, content, { mode: 384 });
|
|
40689
|
+
console.log("\u2713 SAIL_PASSPHRASE saved to .sail/.env.local (mode 0600)");
|
|
40690
|
+
console.log(" sailor run will now work non-interactively.");
|
|
40334
40691
|
} else {
|
|
40335
|
-
this.
|
|
40336
|
-
|
|
40337
|
-
status: "rejected",
|
|
40338
|
-
requestId: msg.requestId,
|
|
40339
|
-
reason: msg.reason
|
|
40340
|
-
},
|
|
40341
|
-
request
|
|
40342
|
-
);
|
|
40692
|
+
console.log("\nTo run non-interactively, add this to .sail/.env.local:");
|
|
40693
|
+
console.log(` SAIL_PASSPHRASE=<your-passphrase>`);
|
|
40343
40694
|
}
|
|
40344
40695
|
}
|
|
40345
|
-
|
|
40346
|
-
|
|
40347
|
-
|
|
40348
|
-
|
|
40349
|
-
|
|
40350
|
-
|
|
40351
|
-
|
|
40352
|
-
|
|
40353
|
-
req.destroy();
|
|
40354
|
-
return;
|
|
40355
|
-
}
|
|
40356
|
-
chunks.push(c);
|
|
40357
|
-
});
|
|
40358
|
-
req.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
|
|
40359
|
-
req.on("error", rej);
|
|
40360
|
-
});
|
|
40696
|
+
}
|
|
40697
|
+
async function keysExportCi() {
|
|
40698
|
+
const account2 = readJsonFile(sailPath("account.json"));
|
|
40699
|
+
const src = resolveKeyPath("manager", account2?.safe);
|
|
40700
|
+
if (!fileExists(src)) {
|
|
40701
|
+
throw new Error(
|
|
40702
|
+
'No agent wallet keystore found.\nComplete Stage 1 (browser UI) to generate your agent wallet, or run\n"sailor keys generate" and choose "agent wallet" to create one manually.'
|
|
40703
|
+
);
|
|
40361
40704
|
}
|
|
40362
|
-
|
|
40363
|
-
|
|
40364
|
-
|
|
40365
|
-
|
|
40705
|
+
const dest = import_node_path8.default.resolve(process.cwd(), "ci-keystore.json");
|
|
40706
|
+
import_node_fs9.default.copyFileSync(src, dest);
|
|
40707
|
+
console.log(`\u2713 Keystore copied to ci-keystore.json`);
|
|
40708
|
+
console.log(` Source: ${src}`);
|
|
40709
|
+
const gitignorePath = import_node_path8.default.resolve(process.cwd(), ".gitignore");
|
|
40710
|
+
if (import_node_fs9.default.existsSync(gitignorePath)) {
|
|
40711
|
+
const content = import_node_fs9.default.readFileSync(gitignorePath, "utf-8");
|
|
40712
|
+
if (!content.includes("ci-keystore.json")) {
|
|
40713
|
+
import_node_fs9.default.appendFileSync(
|
|
40714
|
+
gitignorePath,
|
|
40715
|
+
"\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
|
|
40716
|
+
);
|
|
40717
|
+
console.log("\u2713 Added !ci-keystore.json allowlist entry to .gitignore");
|
|
40718
|
+
} else {
|
|
40719
|
+
console.log("\u2713 .gitignore already tracks ci-keystore.json");
|
|
40366
40720
|
}
|
|
40367
40721
|
}
|
|
40368
|
-
|
|
40369
|
-
|
|
40370
|
-
|
|
40371
|
-
|
|
40372
|
-
|
|
40373
|
-
|
|
40374
|
-
|
|
40375
|
-
|
|
40376
|
-
|
|
40377
|
-
|
|
40378
|
-
|
|
40379
|
-
|
|
40380
|
-
|
|
40381
|
-
|
|
40382
|
-
|
|
40383
|
-
)
|
|
40384
|
-
);
|
|
40722
|
+
console.log("\nNext steps:");
|
|
40723
|
+
console.log(" 1. Add two GitHub Actions secrets (Settings \u2192 Secrets \u2192 Actions):");
|
|
40724
|
+
console.log(" SAIL_PASSPHRASE \u2014 the passphrase that encrypts your agent wallet");
|
|
40725
|
+
console.log(" RPC_URL \u2014 your RPC endpoint");
|
|
40726
|
+
console.log(" 2. Commit and push ci-keystore.json:");
|
|
40727
|
+
console.log(' git add ci-keystore.json && git commit -m "chore: add CI keystore" && git push');
|
|
40728
|
+
console.log("\n The keystore is encrypted \u2014 the raw private key is never exposed.");
|
|
40729
|
+
console.log(" The workflow at .github/workflows/agent-tick.yml unlocks it with SAIL_PASSPHRASE.");
|
|
40730
|
+
}
|
|
40731
|
+
async function keysShow() {
|
|
40732
|
+
const present = ROLES.filter((role) => keyExists(role));
|
|
40733
|
+
if (present.length === 0) {
|
|
40734
|
+
console.log("No keys found in .sail/keys/.");
|
|
40735
|
+
console.log('Run "sailor keys generate" to create one.');
|
|
40736
|
+
return;
|
|
40385
40737
|
}
|
|
40386
|
-
|
|
40387
|
-
|
|
40738
|
+
console.log("Keys in .sail/keys/:\n");
|
|
40739
|
+
for (const role of present) {
|
|
40388
40740
|
try {
|
|
40389
|
-
|
|
40390
|
-
|
|
40741
|
+
const keyring = await loadKeyring(role);
|
|
40742
|
+
console.log(` ${roleLabel(role)}: ${checksum4(keyring.address)}`);
|
|
40743
|
+
} catch (err) {
|
|
40744
|
+
console.log(` ${role}: ${err.message}`);
|
|
40391
40745
|
}
|
|
40392
40746
|
}
|
|
40393
|
-
};
|
|
40394
|
-
async function findAvailablePort(startPort) {
|
|
40395
|
-
return new Promise((res) => {
|
|
40396
|
-
const probe = (0, import_node_net.createServer)();
|
|
40397
|
-
probe.listen(startPort, "127.0.0.1", () => {
|
|
40398
|
-
const addr = probe.address();
|
|
40399
|
-
probe.close(() => res(addr.port));
|
|
40400
|
-
});
|
|
40401
|
-
probe.on("error", () => res(findAvailablePort(startPort + 1)));
|
|
40402
|
-
});
|
|
40403
40747
|
}
|
|
40404
40748
|
|
|
40405
|
-
// src/
|
|
40406
|
-
var
|
|
40407
|
-
var
|
|
40408
|
-
var
|
|
40409
|
-
|
|
40410
|
-
|
|
40411
|
-
|
|
40749
|
+
// src/commands/mandate-contracts.ts
|
|
40750
|
+
var import_node_child_process = require("node:child_process");
|
|
40751
|
+
var import_node_fs10 = require("node:fs");
|
|
40752
|
+
var import_node_path9 = require("node:path");
|
|
40753
|
+
init_esm2();
|
|
40754
|
+
|
|
40755
|
+
// src/lib/mandates.ts
|
|
40756
|
+
var MandateStore = class {
|
|
40757
|
+
filePath;
|
|
40758
|
+
constructor(filePath = sailPath("state", "mandates.json")) {
|
|
40759
|
+
this.filePath = filePath;
|
|
40412
40760
|
}
|
|
40413
|
-
|
|
40414
|
-
|
|
40415
|
-
return
|
|
40761
|
+
read() {
|
|
40762
|
+
const parsed = readJsonFile(this.filePath);
|
|
40763
|
+
return { version: 1, mandates: parsed?.mandates ?? [] };
|
|
40416
40764
|
}
|
|
40417
|
-
|
|
40418
|
-
|
|
40419
|
-
throw new Error(`Signing station not reachable at ${this.baseUrl}`);
|
|
40420
|
-
}
|
|
40765
|
+
write(data) {
|
|
40766
|
+
writeJsonFile(this.filePath, data);
|
|
40421
40767
|
}
|
|
40422
|
-
|
|
40423
|
-
|
|
40768
|
+
list() {
|
|
40769
|
+
return this.read().mandates;
|
|
40424
40770
|
}
|
|
40425
|
-
|
|
40426
|
-
|
|
40427
|
-
|
|
40428
|
-
|
|
40429
|
-
|
|
40430
|
-
|
|
40431
|
-
}
|
|
40771
|
+
/** Find a tracked mandate by address (case-insensitive) or by exact name. */
|
|
40772
|
+
find(addressOrName) {
|
|
40773
|
+
const needle = addressOrName.toLowerCase();
|
|
40774
|
+
return this.read().mandates.find(
|
|
40775
|
+
(m) => m.address.toLowerCase() === needle || m.name === addressOrName
|
|
40776
|
+
);
|
|
40432
40777
|
}
|
|
40433
|
-
|
|
40434
|
-
|
|
40435
|
-
|
|
40436
|
-
|
|
40437
|
-
|
|
40438
|
-
|
|
40439
|
-
|
|
40440
|
-
|
|
40441
|
-
}
|
|
40442
|
-
const { id } = await enqueueRes.json();
|
|
40443
|
-
const deadline = Date.now() + timeoutMs;
|
|
40444
|
-
while (Date.now() < deadline) {
|
|
40445
|
-
const res = await fetch(`${this.baseUrl}/requests/${encodeURIComponent(id)}/result`, {
|
|
40446
|
-
headers: { "x-sailor-secret": this.requestSecret }
|
|
40447
|
-
});
|
|
40448
|
-
if (res.status === 200) return await res.json();
|
|
40449
|
-
if (res.status !== 204) {
|
|
40450
|
-
throw new Error(`Unexpected result status ${res.status} from signing station`);
|
|
40451
|
-
}
|
|
40452
|
-
}
|
|
40453
|
-
throw new Error(`Signing request "${req.title}" timed out after ${timeoutMs / 1e3}s`);
|
|
40778
|
+
/** Append a newly deployed mandate (replacing any prior record at the same address). */
|
|
40779
|
+
add(mandate2) {
|
|
40780
|
+
const data = this.read();
|
|
40781
|
+
data.mandates = data.mandates.filter(
|
|
40782
|
+
(m) => m.address.toLowerCase() !== mandate2.address.toLowerCase()
|
|
40783
|
+
);
|
|
40784
|
+
data.mandates.push(mandate2);
|
|
40785
|
+
this.write(data);
|
|
40454
40786
|
}
|
|
40455
|
-
|
|
40456
|
-
|
|
40457
|
-
|
|
40458
|
-
|
|
40459
|
-
|
|
40460
|
-
|
|
40461
|
-
|
|
40462
|
-
|
|
40463
|
-
if (r.ok) {
|
|
40464
|
-
const { address } = await r.json();
|
|
40465
|
-
if (address) return address;
|
|
40466
|
-
}
|
|
40467
|
-
} catch {
|
|
40468
|
-
}
|
|
40469
|
-
await sleep(1e3);
|
|
40470
|
-
}
|
|
40471
|
-
throw new Error("Timed out waiting for wallet connection in the signing UI");
|
|
40787
|
+
/** Record that a tracked mandate was attached to an SMA. */
|
|
40788
|
+
recordAttachment(address, attachment) {
|
|
40789
|
+
const data = this.read();
|
|
40790
|
+
const mandate2 = data.mandates.find((m) => m.address.toLowerCase() === address.toLowerCase());
|
|
40791
|
+
if (!mandate2) return;
|
|
40792
|
+
mandate2.attachments ??= [];
|
|
40793
|
+
mandate2.attachments.push({ ...attachment, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
40794
|
+
this.write(data);
|
|
40472
40795
|
}
|
|
40473
40796
|
};
|
|
40474
|
-
function readRuntimeServerState(projectRoot) {
|
|
40475
|
-
const file = (0, import_node_path8.join)(projectRoot, RUNTIME_SERVER_FILE);
|
|
40476
|
-
if (!(0, import_node_fs9.existsSync)(file)) return null;
|
|
40477
|
-
try {
|
|
40478
|
-
return JSON.parse((0, import_node_fs9.readFileSync)(file, "utf8"));
|
|
40479
|
-
} catch {
|
|
40480
|
-
return null;
|
|
40481
|
-
}
|
|
40482
|
-
}
|
|
40483
|
-
async function discoverDaemon(projectRoot = process.cwd()) {
|
|
40484
|
-
const state = readRuntimeServerState(projectRoot);
|
|
40485
|
-
if (!state?.url) return null;
|
|
40486
|
-
const client = new SigningClient(state.url, state.requestSecret ?? "");
|
|
40487
|
-
return await client.ping() ? client : null;
|
|
40488
|
-
}
|
|
40489
|
-
async function createSigningChannel(projectRoot = process.cwd()) {
|
|
40490
|
-
const daemon = await discoverDaemon(projectRoot);
|
|
40491
|
-
if (daemon) return daemon;
|
|
40492
|
-
return new SigningServer({ projectRoot, advertise: false });
|
|
40493
|
-
}
|
|
40494
40797
|
|
|
40495
40798
|
// src/commands/onboard.ts
|
|
40496
40799
|
init_esm2();
|
|
@@ -41275,6 +41578,12 @@ async function runDeployClone(project, channel, options) {
|
|
|
41275
41578
|
`Unsupported clone template "${options.template}". Supported: ${Object.keys(CLONE_TEMPLATES).join(", ")}`
|
|
41276
41579
|
);
|
|
41277
41580
|
}
|
|
41581
|
+
const templateMap = project.deployment.standaloneTemplates ?? {};
|
|
41582
|
+
if (Object.keys(templateMap).length === 0) {
|
|
41583
|
+
throw new Error(
|
|
41584
|
+
`No clone templates are available on chain ${project.chainId} yet \u2014 templates are pending redeployment against the new kernel (${project.deployment.kernel}). Deploy your permission directly with \`sailor mandate deploy\` instead.`
|
|
41585
|
+
);
|
|
41586
|
+
}
|
|
41278
41587
|
const impl = project.deployment.standaloneTemplates?.[options.template];
|
|
41279
41588
|
if (!impl || !isAddress(impl, { strict: false })) {
|
|
41280
41589
|
throw new Error(
|
|
@@ -41314,7 +41623,7 @@ async function runDeployClone(project, channel, options) {
|
|
|
41314
41623
|
[sma, impl, BigInt(Math.floor(Date.now() / 1e3))]
|
|
41315
41624
|
)
|
|
41316
41625
|
);
|
|
41317
|
-
const clone = predictCloneAddress(impl, project.contracts.
|
|
41626
|
+
const clone = predictCloneAddress(impl, project.contracts.mandateFactory, submitter, salt);
|
|
41318
41627
|
say(() => {
|
|
41319
41628
|
console.log(`
|
|
41320
41629
|
${spec.label} clone (${options.template})`);
|
|
@@ -41414,7 +41723,7 @@ Connect the owner wallet (mandate signer) in the browser \u2014 the agent wallet
|
|
|
41414
41723
|
args: [sma, impl, salt, initData, deadline, signature]
|
|
41415
41724
|
});
|
|
41416
41725
|
const txHash = await walletClient.sendTransaction({
|
|
41417
|
-
to: project.contracts.
|
|
41726
|
+
to: project.contracts.mandateFactory,
|
|
41418
41727
|
data,
|
|
41419
41728
|
value: fee,
|
|
41420
41729
|
account: agentSigner.viemAccount,
|
|
@@ -43639,6 +43948,7 @@ account.command("predict").description(
|
|
|
43639
43948
|
"--manager <address>",
|
|
43640
43949
|
"Agent (manager) wallet \u2014 mixed into the kernel salt (defaults to .sail/account.json)"
|
|
43641
43950
|
).option("--salt <n>", "CREATE2 salt nonce (default: 0)").option("--chain <id>", "Show prediction for one chain only").option("--json", "Emit machine-readable JSON").action(actionWith(accountPredict));
|
|
43951
|
+
account.command("deploy-chain").description("Deploy the same SMA address on an additional chain using the same owner, manager, and salt").requiredOption("--chain <id>", "Target EVM chain ID (e.g. 8453, 42161, 130, 1)").option("--salt <n>", "CREATE2 salt (defaults to saltNonce stored in .sail/account.json)").option("--json", "Emit machine-readable JSON").action(actionWith(accountDeployChain));
|
|
43642
43952
|
account.command("rotate-signer").description("Rotate the SMA's delegated signer (agent wallet) and re-approve its mandates").option("--sma <address>", "SMA to rotate (defaults to the active account)").option("--to <address>", "Rotate to an existing agent-wallet address instead of generating one").option("--generate", "Generate a fresh local agent wallet (default when --to is omitted)").option("--skip-reattach", "Do not re-approve the previously-attached mandates").option("--reattach-only", "Skip rotation; only re-approve mandates (resume after funding)").option("--json", "Machine-readable output").action(actionWith(rotateSigner));
|
|
43643
43953
|
var mandate = program2.command("mandate").description("Manage mandates");
|
|
43644
43954
|
mandate.command("prepare").description("Prepare a mandate draft for review and signing in the UI (MetaMask)").action(action(mandatePrepare));
|