Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ NEXT_PUBLIC_PRIVY_APP_ID=
NEXT_PUBLIC_RPC_URL=https://api.devnet.solana.com
NEXT_PUBLIC_SOLANA_CLUSTER=devnet
NEXT_PUBLIC_PROGRAM_ID=DygVVPMyA2WnbfJiqta38ySR3YhnKbqbFcNzcsjkti4K
# getProgramAccounts is blocked on most free RPC tiers. The app falls back to
# the public devnet endpoint for those scans automatically; override here only
# if you have a paid RPC that supports gPA.
# NEXT_PUBLIC_GPA_RPC_URL=https://api.devnet.solana.com
# Dedicated RPC for stream list scans (getProgramAccounts). Required when
# NEXT_PUBLIC_RPC_URL is a free-tier provider (Helius/QuickNode) that times out
# or blocks gPA. Transactions still use NEXT_PUBLIC_RPC_URL.
NEXT_PUBLIC_GPA_RPC_URL=https://api.devnet.solana.com

# Mock SPL tokens (devnet). The faucet keypair is the mint authority for all of
# them and is SERVER-ONLY — never add a NEXT_PUBLIC_ prefix to the secret.
Expand Down
41 changes: 18 additions & 23 deletions frontend/lib/anchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@ function getGpaConnection(): Connection {
return gpaConnection;
}

/** True when the RPC rejected getProgramAccounts because of plan/tier limits. */
function isGpaUnavailable(err: unknown): boolean {
const msg = String((err as { message?: string })?.message ?? err);
return /getProgramAccounts is not available|not available on the Free tier|method not found|-32601|-32600/i.test(
msg
);
}

export const idl = idlJson as Idl;

// Decode program accounts with a coder built directly from the IDL. The
Expand Down Expand Up @@ -99,7 +91,7 @@ function isAccountMissing(err: unknown): boolean {
/** True for transient RPC failures worth retrying (rate limit / network). */
function isTransientRpcError(err: unknown): boolean {
const msg = String((err as { message?: string })?.message ?? err);
return /429|too many requests|rate limit|fetch failed|timeout|503|502|ECONNRESET/i.test(
return /429|too many requests|rate limit|fetch failed|timeout|503|502|504|-32504|ECONNRESET/i.test(
msg
);
}
Expand Down Expand Up @@ -584,18 +576,19 @@ export async function fetchStreamsFor(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fetchByOffset = async (offset: number): Promise<{ publicKey: PublicKey; account: any }[]> => {
const filters = [{ memcmp: { offset, bytes: wallet.toBase58() } }];
let raw;
try {
// Try the app's primary RPC first (works if it's a paid tier).
raw = await program.provider.connection.getProgramAccounts(PROGRAM_ID, {
filters,
});
} catch (err) {
// Free tiers block gPA — fall back to the public devnet endpoint that
// allows it. Rethrow anything that isn't a tier/method rejection.
if (!isGpaUnavailable(err)) throw err;
raw = await getGpaConnection().getProgramAccounts(PROGRAM_ID, { filters });
}
const gpaConn = getGpaConnection();

// Heavy gPA scans use the dedicated endpoint (public devnet by default).
// Helius free tier times out (-32504) when used as the primary RPC.
const raw = await withRpcRetry(
async () => gpaConn.getProgramAccounts(PROGRAM_ID, { filters }),
() => {
throw new Error(
"Could not load streams — devnet RPC timed out. Check NEXT_PUBLIC_GPA_RPC_URL and retry."
);
},
3
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decoded: { publicKey: PublicKey; account: any }[] = [];
for (const { pubkey, account } of raw) {
Expand All @@ -611,8 +604,10 @@ export async function fetchStreamsFor(
}
return decoded;
};
const asAuthority = await fetchByOffset(8); // offset of `authority`
const asRecipient = await fetchByOffset(8 + 32); // offset of `recipient`
const [asAuthority, asRecipient] = await Promise.all([
fetchByOffset(8), // offset of `authority`
fetchByOffset(8 + 32), // offset of `recipient`
]);

// Fetch decimals once per unique mint so the UI can render base units
// correctly (mUSDC=6, SOL=9, etc.). Missing/failed → default to 9.
Expand Down
Loading