From 33a761c0c0d59cea0cc64f763323b0804aedc6cc Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Fri, 1 May 2026 11:36:16 +0200 Subject: [PATCH 1/5] Some local fixes --- runbooks/surfnet-setup/main.tx | 1 + webapp/api/server.ts | 71 +++++++++++++++++++ .../components/plan/create-plan-dialog.tsx | 17 ++--- webapp/vite.config.ts | 3 + 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/runbooks/surfnet-setup/main.tx b/runbooks/surfnet-setup/main.tx index ade0cd8..25abb06 100644 --- a/runbooks/surfnet-setup/main.tx +++ b/runbooks/surfnet-setup/main.tx @@ -10,5 +10,6 @@ action "install_subscriptions" "svm::setup_surfnet" { deploy_program { program_id = "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44" binary_path = "target/deploy/subscriptions.so" + idl_path = "idl/subscriptions.json" } } diff --git a/webapp/api/server.ts b/webapp/api/server.ts index 4e1439e..ceba951 100644 --- a/webapp/api/server.ts +++ b/webapp/api/server.ts @@ -16,6 +16,7 @@ const CONFIG_PATH = join(__dirname, '../config.json'); let surfpoolProcess: ChildProcess | null = null; let startingValidator = false; +let deployingProgram = false; const MIN_SOL_AIRDROP = 0.1; const MAX_SOL_AIRDROP = 10; @@ -333,6 +334,70 @@ async function handleStartValidator(): Promise { }); } +async function deployProgramViaSurfnet(): Promise { + if (deployingProgram) return false; + deployingProgram = true; + try { + const soBytes = await readFile(SO_PATH); + const hexData = soBytes.toString('hex'); + log('info', 'Deploying program via surfnet_writeProgram fallback', { size: soBytes.length }); + + const res = await fetch('http://127.0.0.1:8899', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'surfnet_writeProgram', + params: [PROGRAM_ADDRESS, hexData, 0], + }), + signal: AbortSignal.timeout(30_000), + }); + const json = (await res.json()) as { result?: unknown; error?: { message?: string } }; + if (json.error) { + log('error', 'surfnet_writeProgram failed', { error: json.error.message }); + return false; + } + log('info', 'Program deployed via surfnet_writeProgram'); + + // Register IDL if available + const IDL_PATH = join(__dirname, '../../idl/subscriptions.json'); + try { + const idlJson = JSON.parse(await readFile(IDL_PATH, 'utf-8')); + // surfnet_registerIdl expects an 'address' field at the top level + if (!idlJson.address) { + idlJson.address = PROGRAM_ID; + } + const idlRes = await fetch('http://127.0.0.1:8899', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'surfnet_registerIdl', + params: [idlJson], + }), + signal: AbortSignal.timeout(10_000), + }); + const idlResult = (await idlRes.json()) as { result?: unknown; error?: { message?: string } }; + if (idlResult.error) { + log('error', 'surfnet_registerIdl failed', { error: idlResult.error.message }); + } else { + log('info', 'IDL registered via surfnet_registerIdl'); + } + } catch (err) { + log('info', 'IDL registration skipped (file not found or RPC error)', { error: String(err) }); + } + + return true; + } catch (err) { + log('error', 'Fallback program deploy failed', { error: String(err) }); + return false; + } finally { + deployingProgram = false; + } +} + async function handleValidatorStatus(): Promise { let validatorRunning = false; let programDeployed = false; @@ -366,6 +431,12 @@ async function handleValidatorStatus(): Promise { }); const acctJson = (await acctRes.json()) as { result?: { value?: { executable?: boolean } } }; programDeployed = acctJson.result?.value?.executable === true; + + // Fallback: if runbook didn't deploy, do it via RPC cheatcode + if (!programDeployed && !deployingProgram) { + programDeployed = await deployProgramViaSurfnet(); + } + if (programDeployed) { const config = await readConfig(); if (config.networks['localnet']?.programAddress !== programAddress) { diff --git a/webapp/src/components/plan/create-plan-dialog.tsx b/webapp/src/components/plan/create-plan-dialog.tsx index d58a394..96b04ed 100644 --- a/webapp/src/components/plan/create-plan-dialog.tsx +++ b/webapp/src/components/plan/create-plan-dialog.tsx @@ -284,17 +284,18 @@ export function CreatePlanDialog({ open, onOpenChange }: CreatePlanDialogProps)
- setSelectedIcon(e.target.value)} + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" > - {PLAN_ICONS.map(({ name, label, icon: Icon }) => ( - }> + + {PLAN_ICONS.map(({ name, label }) => ( + + ))} - +
diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index 9319fd7..cdd9fea 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -3,6 +3,9 @@ import path from 'path'; import { defineConfig } from 'vite'; export default defineConfig({ + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'), + }, plugins: [react()], resolve: { alias: { From 8a360da98b368cb8bbf2fda0f6eef1680a68a8a3 Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 1 May 2026 12:47:41 -0400 Subject: [PATCH 2/5] fix(api): correct surfnet deploy fallback bugs Two fixes to deployProgramViaSurfnet: - Use PROGRAM_ADDRESS instead of undefined PROGRAM_ID when injecting the address into IDL JSON. Previously threw ReferenceError whenever the IDL lacked a top-level address field. - Kick off the fallback deploy as fire-and-forget rather than awaiting it inside the status handler. The status endpoint is polled by the frontend; awaiting blocked the response for up to 30s during deploy. Next poll picks up executable=true. --- webapp/api/server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webapp/api/server.ts b/webapp/api/server.ts index ceba951..4098ddc 100644 --- a/webapp/api/server.ts +++ b/webapp/api/server.ts @@ -366,7 +366,7 @@ async function deployProgramViaSurfnet(): Promise { const idlJson = JSON.parse(await readFile(IDL_PATH, 'utf-8')); // surfnet_registerIdl expects an 'address' field at the top level if (!idlJson.address) { - idlJson.address = PROGRAM_ID; + idlJson.address = PROGRAM_ADDRESS; } const idlRes = await fetch('http://127.0.0.1:8899', { method: 'POST', @@ -432,9 +432,10 @@ async function handleValidatorStatus(): Promise { const acctJson = (await acctRes.json()) as { result?: { value?: { executable?: boolean } } }; programDeployed = acctJson.result?.value?.executable === true; - // Fallback: if runbook didn't deploy, do it via RPC cheatcode + // Fallback: if runbook didn't deploy, kick off RPC cheatcode deploy + // without blocking status response. Next poll will see executable=true. if (!programDeployed && !deployingProgram) { - programDeployed = await deployProgramViaSurfnet(); + void deployProgramViaSurfnet(); } if (programDeployed) { From 3288e935041848d3c478df0c3267fabca32f209e Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 1 May 2026 12:50:41 -0400 Subject: [PATCH 3/5] fix(webapp): replace nested button with role=button div EnhancedPlanCard wrapped a SolanaButton inside a native +
{expanded && (
From 09ba459133b73c8d99d46f95c3ddf9d64858e632 Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 1 May 2026 13:30:26 -0400 Subject: [PATCH 4/5] ci: extend surfpool deploy wait, skip CU comment on fork PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `idl_path` in surfnet-setup runbook adds an on-chain IDL upload, pushing total deploy time past the 7s retry budget — bump to 30×2s. `pull_request` events on fork PRs ship a read-only GITHUB_TOKEN regardless of the workflow `permissions:` block, so the CU-report comment step 403s. Gate it on same-repo PRs. --- .github/workflows/benchmark.yml | 1 + justfile | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 84d28f1..15d1413 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -30,6 +30,7 @@ jobs: run: just test-and-benchmark - name: Post CU report comment + if: github.event.pull_request.head.repo.full_name == github.repository env: GH_TOKEN: ${{ github.token }} run: | diff --git a/justfile b/justfile index fd68ad9..6cf7577 100644 --- a/justfile +++ b/justfile @@ -148,7 +148,7 @@ ensure-surfpool: > /tmp/surfpool.log 2>&1 & echo $! > .surfpool/pid.txt - for i in {1..7}; do + for i in {1..30}; do if curl -sf -X POST http://localhost:8899 \ -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getAccountInfo\",\"params\":[\"$PROG_ID\",{\"encoding\":\"base64\"}]}" \ @@ -156,8 +156,8 @@ ensure-surfpool: echo "✓ Program deployed successfully ($PROG_ID)" exit 0 fi - echo "Waiting for program deployment... ($i/7)" - sleep 1 + echo "Waiting for program deployment... ($i/30)" + sleep 2 done echo "Error: Program deployment failed" From 3fd015fd944eef9ef42065fc0aebe6a79482a9e2 Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 1 May 2026 14:01:36 -0400 Subject: [PATCH 5/5] fix(runbook): drop idl_path from surfnet-setup deploy_program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit surfpool's setup_surfnet runs an automatic protocol-detection step that attempts an anchor-style IDL conversion. The Codama-emitted IDL has no top-level `address` field, so the conversion errors and the deploy step never completes — leaving the program account empty after start. Result: ts-integration-test sees `getAccountInfo` for the program ID return null indefinitely. Revert to main's content; rely on `just deploy-idl-{devnet,mainnet}` (program-metadata) for on-chain IDL. --- runbooks/surfnet-setup/main.tx | 1 - 1 file changed, 1 deletion(-) diff --git a/runbooks/surfnet-setup/main.tx b/runbooks/surfnet-setup/main.tx index 25abb06..ade0cd8 100644 --- a/runbooks/surfnet-setup/main.tx +++ b/runbooks/surfnet-setup/main.tx @@ -10,6 +10,5 @@ action "install_subscriptions" "svm::setup_surfnet" { deploy_program { program_id = "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44" binary_path = "target/deploy/subscriptions.so" - idl_path = "idl/subscriptions.json" } }