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" diff --git a/webapp/api/server.ts b/webapp/api/server.ts index 4e1439e..4098ddc 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_ADDRESS; + } + 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,13 @@ 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, kick off RPC cheatcode deploy + // without blocking status response. Next poll will see executable=true. + if (!programDeployed && !deployingProgram) { + void 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/src/components/plan/enhanced-collect-payments.tsx b/webapp/src/components/plan/enhanced-collect-payments.tsx index e0439ac..879bf67 100644 --- a/webapp/src/components/plan/enhanced-collect-payments.tsx +++ b/webapp/src/components/plan/enhanced-collect-payments.tsx @@ -303,9 +303,18 @@ function EnhancedPlanCard({ planData, blockTs }: { planData: PlanSubscriberData; return (
- +
{expanded && (
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: {