From 2f67c7b65a31c9ccb00640dc345a520e49b6b57a Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 30 Apr 2026 14:50:52 -0400 Subject: [PATCH 1/6] chore: add .vercelignore for monorepo deploy excludes target/, node_modules/, .git/, .claude/, audits/, runbooks/ so vercel deploy from repo root stays under file count limit. --- .vercelignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .vercelignore diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..140e48c --- /dev/null +++ b/.vercelignore @@ -0,0 +1,17 @@ +**/target +**/node_modules +**/dist +**/.next +**/test-ledger +.validator-ledger +.surfpool +.claude +.git +.emdash.json +.vscode +audits +runbooks +program/target +clients/rust/target +*.log +**/cu_report.md From 7a49c2ba10671bd11753e83049068cce05be9665 Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 30 Apr 2026 14:51:26 -0400 Subject: [PATCH 2/6] fix(webapp): rewrite all paths to index.html for SPA routing direct visits to /faucet, /plans etc. were 404'ing on vercel because the static handler had no fallback to the SPA entrypoint. --- webapp/vercel.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/vercel.json b/webapp/vercel.json index cc3b588..fc58fc5 100644 --- a/webapp/vercel.json +++ b/webapp/vercel.json @@ -3,5 +3,6 @@ "framework": "vite", "buildCommand": "pnpm --filter @subscriptions/client build && pnpm --filter webapp build", "installCommand": "pnpm install --frozen-lockfile", - "outputDirectory": "dist" + "outputDirectory": "dist", + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } From 0658bde2525e970ff154b56e51fdf23315928101 Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 30 Apr 2026 14:57:12 -0400 Subject: [PATCH 3/6] feat(webapp): link to Circle faucet for USDC on devnet in PROD local mint authority isn't available on Vercel, so swap the mint button for an anchor to https://faucet.circle.com/ when not in DEV. --- webapp/src/components/account/account-ui.tsx | 35 ++++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/account/account-ui.tsx b/webapp/src/components/account/account-ui.tsx index e126814..e8f34ae 100644 --- a/webapp/src/components/account/account-ui.tsx +++ b/webapp/src/components/account/account-ui.tsx @@ -415,16 +415,31 @@ export function UsdcFaucetCard() { ))} - {isDevnet &&

Mint authority wallet required

} - + {isDevnet && import.meta.env.DEV && ( +

Mint authority wallet required

+ )} + {isDevnet && !import.meta.env.DEV ? ( + + + + ) : ( + + )} ); From 915c983583070fcb20bcdfbc145e9c8753a41944 Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 30 Apr 2026 15:34:49 -0400 Subject: [PATCH 4/6] fix(webapp): match SOL faucet pattern for USDC card on PROD/devnet Drop amount/recipient inputs and preset buttons in favor of a single inline link to faucet.circle.com, mirroring the existing SOL devnet fallback. --- webapp/src/components/account/account-ui.tsx | 118 ++++++++++--------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/webapp/src/components/account/account-ui.tsx b/webapp/src/components/account/account-ui.tsx index e8f34ae..20b39fc 100644 --- a/webapp/src/components/account/account-ui.tsx +++ b/webapp/src/components/account/account-ui.tsx @@ -373,72 +373,78 @@ export function UsdcFaucetCard() { }); }; + const showCircleLink = isDevnet && !import.meta.env.DEV; + return ( - + - USDC {isDevnet ? 'Mint' : 'Airdrop'} - + USDC {isDevnet ? 'Faucet' : 'Airdrop'} + - setAmount(e.target.value)} - min="1" - step="100" - inputClassName="text-3xl font-bold" - size="xl" - /> - {isDevnet && ( - setRecipient(e.target.value)} - inputClassName="font-mono text-xs" - size="lg" - /> - )} -
- {[100, 1000, 5000, 10000].map(v => ( + {showCircleLink ? ( +

+ USDC airdrop is not available on devnet. Use{' '} + + faucet.circle.com + {' '} + instead. +

+ ) : ( + <> + setAmount(e.target.value)} + min="1" + step="100" + inputClassName="text-3xl font-bold" + size="xl" + /> + {isDevnet && ( + setRecipient(e.target.value)} + inputClassName="font-mono text-xs" + size="lg" + /> + )} +
+ {[100, 1000, 5000, 10000].map(v => ( + + ))} +
+ {isDevnet &&

Mint authority wallet required

} - ))} -
- {isDevnet && import.meta.env.DEV && ( -

Mint authority wallet required

- )} - {isDevnet && !import.meta.env.DEV ? ( - - - - ) : ( - + )}
From cea2e4b9cee83b32768e618735e7d052e58e0a74 Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 30 Apr 2026 15:54:31 -0400 Subject: [PATCH 5/6] fix(webapp): resolve token program from mint owner instead of hardcoding Hardcoded TOKEN_2022_PROGRAM_ADDRESS broke txs against classic-Token mints (e.g. Circle USDC on devnet/mainnet). ATA CreateIdempotent was invoking Token-2022 GetAccountDataSize on a classic mint and bouncing with IncorrectProgramId. Add a small lib helper (resolveTokenProgram) that fetches the mint account once and caches its owner per (rpc, mint). Use it in useSubscriptionsMutations and plan-card init flow so each mutation plumbs the correct token program through to findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstruction, and the overlay builders. --- webapp/src/components/plan/plan-card.tsx | 9 +++-- .../src/hooks/use-subscriptions-mutations.ts | 38 +++++++++++-------- webapp/src/lib/token-program.ts | 15 ++++++++ 3 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 webapp/src/lib/token-program.ts diff --git a/webapp/src/components/plan/plan-card.tsx b/webapp/src/components/plan/plan-card.tsx index e2f336f..9450087 100644 --- a/webapp/src/components/plan/plan-card.tsx +++ b/webapp/src/components/plan/plan-card.tsx @@ -32,7 +32,8 @@ import { useTimeTravel } from '@/hooks/use-time-travel'; import { useWallet } from '@solana/connector/react'; import { address } from '@solana/kit'; import { findAssociatedTokenPda } from '@solana-program/token'; -import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; +import { useClusterConfig } from '@/hooks/use-cluster-config'; +import { resolveTokenProgram } from '@/lib/token-program'; import type { PlanItem } from '@/hooks/use-plans'; import { useMySubscriptions, useSubscriberCount } from '@/hooks/use-subscriptions'; import { PLAN_ICONS, ICON_MAP, parsePlanMeta, type PlanMeta } from '@/lib/plan-constants'; @@ -457,21 +458,23 @@ function SubscribeDialog({ refetch: refetchStatus, } = useSubscriptionAuthorityStatus(plan.data.mint); const { account } = useWallet(); + const { url: rpcUrl } = useClusterConfig(); const amount = Number(plan.data.terms.amount) / USDC_MULTIPLIER; const handleInit = async () => { if (!account) return; const mint = address(plan.data.mint); + const tokenProgram = await resolveTokenProgram(rpcUrl, mint); const [userAta] = await findAssociatedTokenPda({ mint, owner: address(account), - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); initSubscriptionAuthority.mutate( { tokenMint: plan.data.mint, userAta: userAta, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }, { onSuccess: () => refetchStatus() }, ); diff --git a/webapp/src/hooks/use-subscriptions-mutations.ts b/webapp/src/hooks/use-subscriptions-mutations.ts index dd4e373..c25a55e 100644 --- a/webapp/src/hooks/use-subscriptions-mutations.ts +++ b/webapp/src/hooks/use-subscriptions-mutations.ts @@ -1,7 +1,6 @@ import { useKitTransactionSigner } from '@solana/connector/react'; -import { address, createSolanaRpc, type Instruction } from '@solana/kit'; +import { type Address, address, createSolanaRpc, type Instruction } from '@solana/kit'; import { findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstruction } from '@solana-program/token'; -import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; import { fetchMaybeSubscriptionAuthority, findSubscriptionAuthorityPda, @@ -36,6 +35,7 @@ import { type SubscriberPaymentFailure, type SubscriberTransfer, } from '@/lib/collect-utils'; +import { resolveTokenProgram } from '@/lib/token-program'; import { packInstructionBatches } from '@/lib/tx-packer'; import { invalidateWithDelay } from '@/lib/utils'; @@ -51,6 +51,7 @@ export function useSubscriptionsMutations() { const programAddress = useProgramAddress(); const progId = programAddress ? address(programAddress) : undefined; + const resolveTokenProgramForMint = (mint: Address) => resolveTokenProgram(rpcUrl, mint); const initSubscriptionAuthority = useMutation({ mutationFn: async ({ @@ -242,11 +243,12 @@ export function useSubscriptionsMutations() { if (!progId) throw new Error('Program address not configured'); const mint = address(params.tokenMint); + const tokenProgram = await resolveTokenProgramForMint(mint); const delegatorAddr = address(params.delegator); const [delegatorAta] = await findAssociatedTokenPda({ mint, owner: delegatorAddr, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const receiver = params.receiverAta ? address(params.receiverAta) @@ -254,7 +256,7 @@ export function useSubscriptionsMutations() { await findAssociatedTokenPda({ mint, owner: signer.address, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }) )[0]; @@ -263,7 +265,7 @@ export function useSubscriptionsMutations() { mint, owner: signer.address, payer: signer, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const buildFn = @@ -277,7 +279,7 @@ export function useSubscriptionsMutations() { programAddress: progId, receiverAta: receiver, tokenMint: mint, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); return { instructions: [createAtaIx, transferIx], signer }; @@ -334,18 +336,20 @@ export function useSubscriptionsMutations() { if (!signer) throw new Error('Wallet not connected'); if (!progId) throw new Error('Program address not configured'); + const mintAddr = address(mint); + const tokenProgram = await resolveTokenProgramForMint(mintAddr); const instruction = await getCreatePlanOverlayInstructionAsync({ amount, destinations: destinations.map(d => address(d)), endTs: BigInt(endTs), metadataUri, - mint: address(mint), + mint: mintAddr, owner: signer, periodHours: BigInt(periodHours), planId, programAddress: progId, pullers: pullers.map(p => address(p)), - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const signature = await signAndSend([instruction], signer); @@ -564,6 +568,7 @@ export function useSubscriptionsMutations() { if (!progId) throw new Error('Program address not configured'); const mintAddr = address(mint); + const tokenProgram = await resolveTokenProgramForMint(mintAddr); const planPda = address(planAddress); const rpc = createSolanaRpc(rpcUrl); @@ -572,7 +577,7 @@ export function useSubscriptionsMutations() { const [receiverAta] = await findAssociatedTokenPda({ mint: mintAddr, owner: receiverOwner, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const createAtaIx = getCreateAssociatedTokenIdempotentInstruction({ @@ -580,7 +585,7 @@ export function useSubscriptionsMutations() { mint: mintAddr, owner: receiverOwner, payer: signer, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const { payable, failures: preflightFailures } = await filterPayableSubscribers({ @@ -588,7 +593,7 @@ export function useSubscriptionsMutations() { programAddress: progId, rpc, subscribers, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const transferEntries: SubscriberTransfer[] = await Promise.all( @@ -602,7 +607,7 @@ export function useSubscriptionsMutations() { receiverAta, subscriptionPda: address(sub.subscriptionAddress), tokenMint: mintAddr, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); return { instruction, subscriber: sub }; }), @@ -682,6 +687,7 @@ export function useSubscriptionsMutations() { for (const plan of plans) { const mintAddr = address(plan.mint); + const tokenProgram = await resolveTokenProgramForMint(mintAddr); const planPda = address(plan.planAddress); const subscribersWithPlan = plan.subscribers.map(sub => ({ ...sub, @@ -692,7 +698,7 @@ export function useSubscriptionsMutations() { programAddress: progId, rpc, subscribers: subscribersWithPlan, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); preflightFailures.push(...failures); if (payable.length === 0) continue; @@ -702,7 +708,7 @@ export function useSubscriptionsMutations() { const [receiverAta] = await findAssociatedTokenPda({ mint: mintAddr, owner: receiverOwner, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); const ataKey = receiverAta.toString(); @@ -714,7 +720,7 @@ export function useSubscriptionsMutations() { mint: mintAddr, owner: receiverOwner, payer: signer, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }), ); } @@ -729,7 +735,7 @@ export function useSubscriptionsMutations() { receiverAta, subscriptionPda: address(sub.subscriptionAddress), tokenMint: mintAddr, - tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenProgram, }); transferEntries.push({ instruction, subscriber: sub }); } diff --git a/webapp/src/lib/token-program.ts b/webapp/src/lib/token-program.ts new file mode 100644 index 0000000..8282a69 --- /dev/null +++ b/webapp/src/lib/token-program.ts @@ -0,0 +1,15 @@ +import { createSolanaRpc, type Address } from '@solana/kit'; + +const cache = new Map(); + +export async function resolveTokenProgram(rpcUrl: string, mint: Address): Promise
{ + const key = `${rpcUrl}:${mint.toString()}`; + const cached = cache.get(key); + if (cached) return cached; + const rpc = createSolanaRpc(rpcUrl); + const info = await rpc.getAccountInfo(mint, { encoding: 'base64' }).send(); + if (!info.value) throw new Error(`Mint ${mint.toString()} not found`); + const owner = info.value.owner; + cache.set(key, owner); + return owner; +} From 8663e2bda43a2f2e295ce912c73c760d8c140161 Mon Sep 17 00:00:00 2001 From: Jo D Date: Thu, 30 Apr 2026 16:03:48 -0400 Subject: [PATCH 6/6] chore(webapp): ignore .vercel link config added by vercel CLI on link. --- webapp/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/.gitignore b/webapp/.gitignore index b1cf772..7823f49 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -29,3 +29,4 @@ dist-ssr # Generated code src/generated/ +.vercel