Skip to content

Commit 6b883f9

Browse files
committed
feat: clarify Permit2 approval requirement after bot create
- Add `needsPermit2Approval` flag to create response; always true since on-chain allowances are not verified at creation time - Update create endpoint messaging to explicitly name Permit2, mention gas costs, and guide users to approve before dry-run or live start - Add warning banner in OneShotRunner to highlight Permit2 + gas requirement when `highlightPermit2` is set - Land on Tools tab after successful bot creation to make "Approve allowances" the next obvious step - Display persistent Permit2 approval warning banner on BotDetail page that dismisses after approval succeeds - Update confirmation dialogs and help text throughout to reference "Permit2" instead of vague "router allowance" language - Clarify that approval transactions cost "a little gas" and are one-time operations per input token - Add callback `onApproved` to OneShotRunner to dismiss Permit2 banner when approve action succeeds
1 parent a9ca083 commit 6b883f9

11 files changed

Lines changed: 119 additions & 22 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d6793d883ce3f83e24a797330e516b57419da1c3
1+
a953771c46c910ce1507c781200778ef395adb98

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.163
1+
0.1.164

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.163"
3+
version = "0.1.164"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; market-makes the filler order book with signed UniswapX limit orders."
66
license = "AGPL-3.0-or-later"

src/panel/http/wizard.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ pub struct CreateRequest {
253253
pub corridor_id: String,
254254
pub signer: SignerRequest,
255255
/// Start the bot immediately. Off by default: the recommended path is to
256-
/// approve the router allowance and dry-run first.
256+
/// approve Permit2 (costs a little gas) and dry-run first.
257257
#[serde(default)]
258258
pub start: bool,
259259
}
@@ -411,22 +411,34 @@ pub async fn create(
411411
// Re-read for the response — the wallet comes from the config just written, and the
412412
// state changed if the start took.
413413
let (bot, fleet) = state.bot_and_fleet(&name).await?;
414+
// Create never checks on-chain Permit2 allowances. `docker.start` succeeding
415+
// only means the container launched — the bot's own preflight can still fail
416+
// and restart-loop if approvals are missing. Always tell the UI to surface
417+
// the Permit2 handoff; Approve is a no-op when allowances are already set.
418+
let needs_permit2_approval = true;
414419
Ok((
415420
axum::http::StatusCode::CREATED,
416421
Json(serde_json::json!({
417422
"bot": bots::to_body(&bot, &state, &fleet),
423+
"needsPermit2Approval": needs_permit2_approval,
418424
"message": format!(
419425
"{name} is set up for {} on {}. {}",
420426
corridor.display_name,
421427
corridor.network_label,
422428
match (&start_error, started) {
423429
(Some(reason), _) => format!(
424430
"It was created but not started: {reason} Start it from its page once that \
425-
clears."
431+
clears. Approve Permit2 first if that was the blocker (needs a little gas)."
426432
),
427-
(None, true) => "It's running — check its logs.".to_string(),
433+
(None, true) => {
434+
"It's running — check its logs. If it restart-loops, approve Permit2 \
435+
(Tools → Approve allowances — needs a little gas)."
436+
.to_string()
437+
},
428438
(None, false) => {
429-
"Approve the router allowance and dry-run it before starting.".to_string()
439+
"Next: approve Permit2 for its input tokens (Tools → Approve \
440+
allowances — needs a little gas), then dry-run before starting."
441+
.to_string()
430442
}
431443
}
432444
),
@@ -630,6 +642,18 @@ mod tests {
630642
let v = Harness::parse(&body);
631643
assert_eq!(v["bot"]["running"], false);
632644
assert!(v["message"].as_str().unwrap().contains("dry-run"), "{body}");
645+
assert!(
646+
v["message"].as_str().unwrap().contains("Permit2"),
647+
"create message should name Permit2: {body}"
648+
);
649+
assert!(
650+
v["message"].as_str().unwrap().contains("gas"),
651+
"create message should mention gas: {body}"
652+
);
653+
assert_eq!(
654+
v["needsPermit2Approval"], true,
655+
"create must hand off Permit2 regardless of container state: {body}"
656+
);
633657
// The response must not echo the key back.
634658
assert!(!body.contains(TEST_KEY));
635659
}

web/src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
ActionResult,
1010
Bot,
1111
Corridor,
12+
CreateBotResult,
1213
Fleet,
1314
MigrationResult,
1415
SaveResult,
@@ -139,7 +140,7 @@ export const api = {
139140
corridors: () => request<{ corridors: Corridor[] }>('/api/corridors'),
140141

141142
createBot: (body: unknown) =>
142-
request<{ bot: Bot; message: string }>('/api/bots', json(body)),
143+
request<CreateBotResult>('/api/bots', json(body)),
143144

144145
/**
145146
* Mint a fresh hot wallet for the Create wallet step. Returns address + seed

web/src/components/OneShotRunner.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ export default function OneShotRunner({
2929
bot,
3030
canApprove,
3131
approveBlockedReason,
32+
highlightPermit2 = false,
33+
onApproved,
3234
}: {
3335
bot: string
3436
canApprove: boolean
3537
approveBlockedReason: string | null
38+
/** After create: surface the Permit2 + gas requirement up front. */
39+
highlightPermit2?: boolean
40+
/** Fired when `stitch approve` exits successfully. */
41+
onApproved?: () => void
3642
}) {
3743
const [lines, setLines] = useState<LogLine[]>([])
3844
const [running, setRunning] = useState<string | null>(null)
@@ -50,7 +56,7 @@ export default function OneShotRunner({
5056
if (
5157
action === 'approve' &&
5258
!window.confirm(
53-
`Approve the router allowances for ${bot}? This sends transactions from its operator wallet and costs gas.`,
59+
`Approve Permit2 for ${bot}'s input tokens?\n\nThis sends transactions from its operator wallet and costs a little gas. Without this approval the bot cannot trade.`,
5460
)
5561
) {
5662
return
@@ -70,8 +76,12 @@ export default function OneShotRunner({
7076
if (event === 'line') {
7177
setLines((prev) => appendLine(prev, data as LogLine))
7278
} else if (event === 'exit') {
73-
setExit(data as ExitEvent)
79+
const result = data as ExitEvent
80+
setExit(result)
7481
setRunning(null)
82+
if (result.ok && result.action === 'approve') {
83+
onApproved?.()
84+
}
7585
} else if (event === 'error') {
7686
setError((data as { message: string }).message)
7787
setRunning(null)
@@ -88,6 +98,15 @@ export default function OneShotRunner({
8898

8999
return (
90100
<div className="space-y-3">
101+
{highlightPermit2 && (
102+
<Banner tone="warning">
103+
Before the first live start, run <strong>Approve allowances</strong>. That
104+
grants Permit2 permission on each input token. The operator wallet needs a
105+
little native gas for those one-time approval transactions — without them,
106+
orders would post but fail to fill.
107+
</Banner>
108+
)}
109+
91110
<div className="flex flex-wrap items-center gap-2">
92111
<Button
93112
busy={running === 'dry-run'}
@@ -103,7 +122,7 @@ export default function OneShotRunner({
103122
onClick={() => run('approve')}
104123
title={
105124
approveBlockedReason ??
106-
'Grant the allowances the bot needs to trade. Sends transactions.'
125+
'Approve input tokens to Permit2. Sends transactions and costs gas.'
107126
}
108127
>
109128
Approve allowances
@@ -122,8 +141,9 @@ export default function OneShotRunner({
122141
</div>
123142

124143
<p className="text-xs text-faint">
125-
Both run in a throwaway container with this bot's own config and key. A dry
126-
run posts nothing; approve sends transactions and costs gas.
144+
Both run in a throwaway container with this bot&apos;s own config and key. A
145+
dry run posts nothing; Approve allowances sends Permit2 approval
146+
transactions and costs a little gas.
127147
</p>
128148

129149
{!canApprove && approveBlockedReason && (

web/src/components/SettingsForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ function CorridorCard({
269269
async function apply() {
270270
if (
271271
!window.confirm(
272-
`Switch ${bot.name} to a different corridor?\n\nThis replaces stitch.toml with the preset (your signer is kept). A running bot is stopped — approve tokens for the new corridor before starting.`,
272+
`Switch ${bot.name} to a different corridor?\n\nThis replaces stitch.toml with the preset (your signer is kept). A running bot is stopped — approve Permit2 for the new corridor's tokens (needs a little gas) before starting.`,
273273
)
274274
) {
275275
return

web/src/pages/AddBot.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ export default function AddBot() {
9393
// Clear the secret from component state the moment it's no longer needed.
9494
setSigner(emptySigner)
9595
navigate(`/bots/${encodeURIComponent(res.bot.name)}`, {
96-
state: { note: res.message },
96+
state: {
97+
note: res.message,
98+
// From the create API — not `bot.running`. Docker can report running
99+
// before the bot's Permit2 preflight finishes (and fails).
100+
needsPermit2: res.needsPermit2Approval,
101+
},
97102
})
98103
} catch (e) {
99104
setError(e instanceof ApiError ? e.message : String(e))
@@ -196,9 +201,10 @@ export default function AddBot() {
196201
/>
197202
{!start && (
198203
<p className="text-xs text-faint">
199-
Left off, the bot is created but stopped. Approve the router
200-
allowance and dry-run it from its page first — that's the safer
201-
order.
204+
Left off, the bot is created but stopped. On its page, approve
205+
Permit2 for the input tokens (needs a little gas on the operator
206+
wallet), then dry-run — that&apos;s the safer order before the
207+
first live start.
202208
</p>
203209
)}
204210

web/src/pages/BotDetail.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,19 @@ export default function BotDetail() {
3636
const [error, setError] = useState<string | null>(null)
3737
// The wizard redirects here with what it just did, so its confirmation survives
3838
// the navigation.
39-
const handoff = (useLocation().state as { note?: string } | null)?.note ?? null
40-
const [note, setNote] = useState<string | null>(handoff)
39+
const handoff = useLocation().state as {
40+
note?: string
41+
needsPermit2?: boolean
42+
} | null
43+
const [note, setNote] = useState<string | null>(handoff?.note ?? null)
44+
const [showPermit2Banner, setShowPermit2Banner] = useState(
45+
() => !!handoff?.needsPermit2,
46+
)
4147
const [busy, setBusy] = useState<string | null>(null)
42-
const [tab, setTab] = useState<Tab>('settings')
48+
// After create, land on Tools so Approve allowances is the next obvious step.
49+
const [tab, setTab] = useState<Tab>(() =>
50+
handoff?.needsPermit2 ? 'tools' : 'settings',
51+
)
4352
const [updates, setUpdates] = useState<UpdatesStatus | null>(null)
4453

4554
const load = useCallback(async () => {
@@ -197,6 +206,30 @@ export default function BotDetail() {
197206
</Banner>
198207
)}
199208

209+
{showPermit2Banner && (
210+
<Banner tone="warning" onDismiss={() => setShowPermit2Banner(false)}>
211+
<div className="space-y-2">
212+
<p>
213+
<strong>Permit2 approval required.</strong> This bot cannot trade
214+
until its operator wallet approves Permit2 for each input token.
215+
That is a one-time on-chain step and needs a little native gas on
216+
the wallet (for the approval transactions).
217+
</p>
218+
<p>
219+
Open{' '}
220+
<button
221+
type="button"
222+
className="font-bold underline hover:no-underline"
223+
onClick={() => setTab('tools')}
224+
>
225+
Tools → Approve allowances
226+
</button>
227+
, then dry-run, then Start.
228+
</p>
229+
</div>
230+
</Banner>
231+
)}
232+
200233
<Card>
201234
{/*
202235
On narrow screens a single flex+ml-auto row wraps badly: Update stays
@@ -366,6 +399,8 @@ export default function BotDetail() {
366399
bot={bot.name}
367400
canApprove={bot.canApprove}
368401
approveBlockedReason={bot.approveBlockedReason}
402+
highlightPermit2={showPermit2Banner}
403+
onApproved={() => setShowPermit2Banner(false)}
369404
/>
370405
</Card>
371406
)}

0 commit comments

Comments
 (0)