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
4 changes: 3 additions & 1 deletion packages/blockchain-api-client/src/contracts/fiat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
UpdateTransferInputSchema,
UpdateTransferOutputSchema,
} from "../schemas/fiat";
import { BAD_REQUEST, NOT_FOUND, UNAUTHENTICATED, UNAUTHORIZED } from "../errors/common";
import { BAD_REQUEST, NOT_FOUND, RATE_LIMITED, UNAUTHENTICATED, UNAUTHORIZED } from "../errors/common";
import { INSUFFICIENT_FUNDS } from "../errors/solana";
import { oc } from "@orpc/contract";

Expand Down Expand Up @@ -72,6 +72,7 @@ export const fiatContract = oc
.errors({
BAD_REQUEST,
JUPITER_ERROR: { message: "Failed to get quote from Jupiter", status: 500 },
RATE_LIMITED,
}),
sendFunds: oc
.route({ method: "POST", path: "/fiat/send" })
Expand All @@ -82,6 +83,7 @@ export const fiatContract = oc
BRIDGE_ERROR: { message: "Failed to create Bridge transfer", status: 500 },
JUPITER_ERROR: { message: "Failed to get quote from Jupiter", status: 500 },
INSUFFICIENT_FUNDS,
RATE_LIMITED,
}),
updateTransfer: oc
.route({ method: "PUT", path: "/fiat/transfer/{id}" })
Expand Down
4 changes: 4 additions & 0 deletions packages/blockchain-api-client/src/contracts/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TransactionDataSchema,
} from "../schemas/common";
import { INSUFFICIENT_FUNDS } from "../errors/solana";
import { RATE_LIMITED } from "../errors/common";

export const swapContract = oc
.tag("Swap")
Expand All @@ -20,6 +21,7 @@ export const swapContract = oc
.output(TokenListOutputSchema)
.errors({
JUPITER_ERROR: { message: "Failed to fetch tokens from Jupiter" },
RATE_LIMITED,
}),
getQuote: oc
.route({ method: "GET", path: "/swap/quote", })
Expand All @@ -28,6 +30,7 @@ export const swapContract = oc
.errors({
BAD_REQUEST: { message: "Invalid quote parameters", status: 400 },
JUPITER_ERROR: { message: "Failed to get quote from Jupiter" },
RATE_LIMITED,
}),
getInstructions: oc
.route({ method: "POST", path: "/swap/instructions", })
Expand All @@ -37,5 +40,6 @@ export const swapContract = oc
BAD_REQUEST: { message: "Invalid instruction parameters", status: 400 },
JUPITER_ERROR: { message: "Failed to get swap instructions from Jupiter" },
INSUFFICIENT_FUNDS,
RATE_LIMITED,
}),
});
1 change: 1 addition & 0 deletions packages/blockchain-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ ENV NEXT_PUBLIC_PRIVY_APP_ID="__NEXT_PUBLIC_PRIVY_APP_ID__" \
NEXT_PUBLIC_SOLANA_CLUSTER="__NEXT_PUBLIC_SOLANA_CLUSTER__" \
NEXT_PUBLIC_SOLANA_URL="__NEXT_PUBLIC_SOLANA_URL__" \
NEXT_PUBLIC_WORLD_HELIUM_URL="__NEXT_PUBLIC_WORLD_HELIUM_URL__" \
SENTRY_DSN="__SENTRY_DSN__" \
NODE_ENV=production \
NODE_OPTIONS="--max-old-space-size=4096"

Expand Down
2 changes: 2 additions & 0 deletions packages/blockchain-api/sentry.edge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ Sentry.init({
enabled: !!process.env.SENTRY_DSN,
release: process.env.SENTRY_RELEASE,
tracesSampleRate: 1,
normalizeDepth: 10,
normalizeMaxBreadth: 2000,
sendDefaultPii: true,
});
5 changes: 5 additions & 0 deletions packages/blockchain-api/sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ if (process.env.NODE_ENV === "production") {
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,

// Default is 3; our extras (e.g. bundle simulation transaction_results with
// nested err/logs) need more depth or Sentry shows [Object]/[Array]
normalizeDepth: 10,
normalizeMaxBreadth: 2000,

// Enable sending user PII (Personally Identifiable Information)
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
Expand Down
38 changes: 31 additions & 7 deletions packages/blockchain-api/src/lib/utils/jito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { populateMissingDraftInfo, toVersionedTx } from "@helium/spl-utils";
import * as Sentry from "@sentry/nextjs";
import { env } from "../env";
import { classifySimulationLogs } from "./simulation-classifier";
import { getChewingGlassExplorerUrl, getExplorerUrl } from "./explorer";

export function shouldUseJitoBundle(
transactionsLength: number,
Expand Down Expand Up @@ -193,12 +194,12 @@ export async function simulateJitoBundle(
serializedTransactions: string[],
context?: JitoBundleContext,
): Promise<void> {
const base64Txs = serializedTransactions.map((tx) => {
const transaction = VersionedTransaction.deserialize(
Buffer.from(tx, "base64"),
);
return Buffer.from(transaction.serialize()).toString("base64");
});
const deserializedTxs = serializedTransactions.map((tx) =>
VersionedTransaction.deserialize(Buffer.from(tx, "base64")),
);
const base64Txs = deserializedTxs.map((transaction) =>
Buffer.from(transaction.serialize()).toString("base64"),
);

const nullConfigs = base64Txs.map(() => null);
const response = await jitoRpcRequest("simulateBundle", [
Expand Down Expand Up @@ -245,7 +246,16 @@ export async function simulateJitoBundle(

if (simulation && simulation.summary !== "succeeded") {
const summaryStr = stringifySummary(simulation.summary);
const txResults = simulation.transactionResults;
// Drop pre/postExecutionAccounts: we requested null configs, so these are
// noise that just clutters Sentry's normalized depth budget.
const txResults = simulation.transactionResults.map((r) => {
const {
preExecutionAccounts: _pre,
postExecutionAccounts: _post,
...rest
} = r as Record<string, unknown>;
return rest as typeof r;
});
const allLogs = txResults.flatMap((r) => r.logs ?? []);
const { category, detail } = classifyBundleSimulationFailure(txResults);
const actionType = deriveActionType(context);
Expand Down Expand Up @@ -291,6 +301,20 @@ export async function simulateJitoBundle(
tag: context?.tag,
payer: context?.payer,
transaction_metadata: context?.transactionMetadata,
explorer_links: deserializedTxs.map((tx) => {
try {
return getExplorerUrl(tx);
} catch {
return null;
}
}),
chewing_glass_explorer_links: deserializedTxs.map((tx) => {
try {
return getChewingGlassExplorerUrl(tx);
} catch {
return null;
}
}),
},
});
throw err;
Expand Down
6 changes: 6 additions & 0 deletions packages/blockchain-api/src/server/api/routers/fiat/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ const getSendQuote = publicProcedure.fiat.getSendQuote.handler(
);

if (!quoteResponse.ok) {
if (quoteResponse.status === 429) {
throw errors.RATE_LIMITED();
}
throw errors.JUPITER_ERROR({
message: "Failed to get quote from Jupiter",
});
Expand Down Expand Up @@ -548,6 +551,9 @@ const sendFunds = publicProcedure.fiat.sendFunds.handler(
},
);

if (!instructionsResponse.ok && instructionsResponse.status === 429) {
throw errors.RATE_LIMITED();
}
const instructions = await instructionsResponse.json();
if (instructions.error) {
throw errors.JUPITER_ERROR({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,25 @@ export const create = publicProcedure.governance.createPosition.handler(
});
}

// depositV0 CPIs a token transfer of `amount` from the user's ATA —
// verify the user actually holds enough of the deposit mint, otherwise
// the on-chain tx fails with an opaque SPL token Custom(1) instead of
// a clean INSUFFICIENT_FUNDS response.
const depositAta = getAssociatedTokenAddressSync(mintPubkey, walletPubkey);
const depositAtaInfo = await connection
.getTokenAccountBalance(depositAta)
.catch(() => null);
const depositAvailable = new BN(depositAtaInfo?.value.amount ?? "0");
if (depositAvailable.lt(amount)) {
throw errors.INSUFFICIENT_FUNDS({
message: `Insufficient ${TOKEN_NAMES[tokenAmount.mint] ?? "token"} balance to create position`,
data: {
required: amount.toNumber(),
available: depositAvailable.toNumber(),
},
});
}

return {
transactionData: {
transactions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import { TASK_QUEUE_ID } from "@/lib/constants/tuktuk";
// Max hotspots to process per direct claim call (applies to all networks)
const MAX_DIRECT_CLAIM_HOTSPOTS = 4;
// HNT wallets with more than this many hotspots use Tuktuk instead of direct claim
const MAX_HNT_DIRECT_CLAIM_TOTAL = 12;
const MAX_HNT_DIRECT_CLAIM_TOTAL = 50;

/**
* Create transactions to claim rewards for all hotspots in a wallet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export const getInstructions = publicProcedure.swap.getInstructions.handler(
if (!instructionsResponse.ok) {
const errorText = await instructionsResponse.text();
console.error("Jupiter API error:", errorText);
if (instructionsResponse.status === 429) {
throw errors.RATE_LIMITED();
}
throw errors.JUPITER_ERROR({
message: `Failed to get swap instructions from Jupiter: HTTP ${instructionsResponse.status}: ${errorText.slice(0, 500)}`,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export const getQuote = publicProcedure.swap.getQuote.handler(
});
}

// Surface Jupiter rate limiting as a 429 so clients can back off instead
// of us spamming Sentry with JUPITER_ERROR 500s.
if (quoteResponse.status === 429) {
throw errors.RATE_LIMITED();
}

throw errors.JUPITER_ERROR({
message: `Failed to get quote from Jupiter: HTTP ${quoteResponse.status}: ${errorText.slice(0, 500)}`,
});
Expand Down
2 changes: 1 addition & 1 deletion packages/blockchain-api/tests/e2e/governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ describe("governance", () => {
)
);
tx.message.recentBlockhash = blockhash;
tx.sign([ctx.payer, recipient]);
tx.sign([ctx.payer]);
const sig = await ctx.connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
});
Expand Down
Loading