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 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 diff --git a/webapp/src/components/account/account-ui.tsx b/webapp/src/components/account/account-ui.tsx index e126814..20b39fc 100644 --- a/webapp/src/components/account/account-ui.tsx +++ b/webapp/src/components/account/account-ui.tsx @@ -373,58 +373,79 @@ 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 &&

Mint authority wallet required

} - + + )}
); 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; +} 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" }] }