diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0eb547277..627c19bd1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -141,6 +141,7 @@ jobs: - packages/crons - packages/geocoder-service - packages/recent-helium-transactions-service + - packages/blockchain-api steps: - uses: actions/checkout@v3 - uses: ./.github/actions/build-anchor/ diff --git a/packages/blockchain-api/Dockerfile b/packages/blockchain-api/Dockerfile index d6144e33e..b7e1f5ca7 100644 --- a/packages/blockchain-api/Dockerfile +++ b/packages/blockchain-api/Dockerfile @@ -31,9 +31,7 @@ ARG SENTRY_ORG ARG SENTRY_PROJECT ARG SENTRY_RELEASE -# Skip server env validation at build time — real values injected at runtime -ENV SKIP_ENV_VALIDATION=1 \ - NEXT_PUBLIC_PRIVY_APP_ID="__NEXT_PUBLIC_PRIVY_APP_ID__" \ +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__" \ @@ -46,7 +44,17 @@ ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ SENTRY_PROJECT=${SENTRY_PROJECT} \ SENTRY_RELEASE=${SENTRY_RELEASE} -RUN pnpm turbo run build --filter=@helium/blockchain-api-service +RUN printf '%s\n' \ + 'SKIP_ENV_VALIDATION=1' \ + 'PG_USER=build' \ + 'PG_NAME=build' \ + 'PG_HOST=localhost' \ + 'PG_PORT=5432' \ + 'PRIVY_APP_SECRET=build' \ + 'BRIDGE_API_KEY=build' \ + 'JUPITER_API_KEY=build' \ + > packages/blockchain-api/.env && \ + pnpm turbo run build --filter=@helium/blockchain-api-service # Production image, copy all the files and run next FROM node:22-alpine diff --git a/packages/blockchain-api/src/app/api/v1/[[...rest]]/route.ts b/packages/blockchain-api/src/app/api/v1/[[...rest]]/route.ts index bf0f3714c..fce164be1 100644 --- a/packages/blockchain-api/src/app/api/v1/[[...rest]]/route.ts +++ b/packages/blockchain-api/src/app/api/v1/[[...rest]]/route.ts @@ -7,6 +7,8 @@ import { onError } from "@orpc/server"; import { ORPCError } from "@orpc/server"; import * as Sentry from "@sentry/nextjs"; +export const dynamic = "force-dynamic"; + /** * ORPC OpenAPI handler for the public v1 API. * diff --git a/packages/blockchain-api/src/app/layout.tsx b/packages/blockchain-api/src/app/layout.tsx index bb0cd13e7..5694f0515 100644 --- a/packages/blockchain-api/src/app/layout.tsx +++ b/packages/blockchain-api/src/app/layout.tsx @@ -4,7 +4,6 @@ import { Geist, Geist_Mono } from "next/font/google"; import React, { Suspense } from "react"; import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; -import "../lib/background-jobs/transaction-resubmission"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -31,14 +30,14 @@ export default function RootLayout({ />
-
+
{children} diff --git a/packages/blockchain-api/src/app/rpc/[[...rest]]/route.ts b/packages/blockchain-api/src/app/rpc/[[...rest]]/route.ts index 5f422c0aa..1fa8dfdd9 100644 --- a/packages/blockchain-api/src/app/rpc/[[...rest]]/route.ts +++ b/packages/blockchain-api/src/app/rpc/[[...rest]]/route.ts @@ -6,6 +6,8 @@ import { ORPCError } from "@orpc/server"; import { ValidationError } from "@orpc/server"; import * as Sentry from "@sentry/nextjs"; +export const dynamic = "force-dynamic"; + const rpcHandler = new RPCHandler(appRouter, { interceptors: [ onError((error) => { @@ -26,7 +28,7 @@ const rpcHandler = new RPCHandler(appRouter, { Object.entries(value).map(([key, val]) => [ key, serializeValue(val), - ]), + ]) ); } catch { // If serialization fails, convert to string @@ -54,8 +56,8 @@ const rpcHandler = new RPCHandler(appRouter, { data: serializeValue(error.cause.data), } : error.cause - ? serializeValue(error.cause) - : undefined, + ? serializeValue(error.cause) + : undefined, }; console.error("RPC Error:", JSON.stringify(errorDetails, null, 2)); @@ -147,7 +149,7 @@ async function handleRequest(request: Request) { if (!response && isBatchPath) { console.error( "[RPC] BatchHandlerPlugin failed to handle batch request. " + - "This might indicate a bug in ORPC or incorrect configuration.", + "This might indicate a bug in ORPC or incorrect configuration." ); } diff --git a/packages/blockchain-api/src/instrumentation.ts b/packages/blockchain-api/src/instrumentation.ts index ecb65282b..2f57e0be9 100644 --- a/packages/blockchain-api/src/instrumentation.ts +++ b/packages/blockchain-api/src/instrumentation.ts @@ -3,6 +3,16 @@ import * as Sentry from "@sentry/nextjs"; export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("../sentry.server.config"); + if (process.env.NO_PG !== "true") { + try { + const { transactionResubmissionService } = await import( + "./lib/background-jobs/transaction-resubmission" + ); + transactionResubmissionService.start(); + } catch (e) { + console.error("Failed to start transaction resubmission service:", e); + } + } } if (process.env.NEXT_RUNTIME === "edge") { diff --git a/packages/blockchain-api/src/lib/db.ts b/packages/blockchain-api/src/lib/db.ts index 634e7c017..5778c0bb3 100644 --- a/packages/blockchain-api/src/lib/db.ts +++ b/packages/blockchain-api/src/lib/db.ts @@ -12,7 +12,7 @@ if (!POSTGRES_URL) { throw new Error("POSTGRES_URL environment variable is not set"); } else { console.warn( - "POSTGRES_URL environment variable is not set. Using default for development.", + "POSTGRES_URL environment variable is not set. Using default for development." ); } } @@ -20,16 +20,19 @@ if (!POSTGRES_URL) { // For serverless environments, we want to limit the pool size // For Docker/standalone, we can use a larger pool export const isServerless = process.env.VERCEL === "1"; -const poolConfig = isServerless +const noPg = process.env.NO_PG === "true"; +const poolConfig = noPg + ? { max: 1, min: 0, acquire: 1000, idle: 1000 } + : isServerless ? { - max: 1, // Serverless should use minimal connections - acquire: 30000, // Time to wait for a connection (30 seconds) - idle: 10000, // Time before connection is released (10 seconds) + max: 1, + acquire: 30000, + idle: 10000, } : { - max: 20, // Docker can use more connections + max: 20, min: 5, - acquire: 60000, // More generous timeouts for Docker + acquire: 60000, idle: 10000, }; @@ -61,7 +64,7 @@ export const sequelize = new Sequelize(POSTGRES_URL, { return reject(err); } resolve(token); - }), + }) ); config.dialectOptions = { ssl: { diff --git a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts index 61ac91875..6cf34aaea 100644 --- a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts +++ b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts @@ -49,7 +49,7 @@ type InputDeploymentInfo = Extract< // Convert input deploymentInfo to onboarding format (partial) function inputToOnboardingDeploymentInfo( - info: InputDeploymentInfo | undefined, + info: InputDeploymentInfo | undefined ): MobileDeploymentInfoV0 | undefined { if (!info) return undefined; @@ -70,7 +70,7 @@ function inputToOnboardingDeploymentInfo( // null = unset the field, undefined = use the prior value function mergeDeploymentInfo( existing: MobileDeploymentInfoV0 | null | undefined, - newInfo: InputDeploymentInfo | undefined, + newInfo: InputDeploymentInfo | undefined ): MobileDeploymentInfoV0 | undefined { if (!newInfo) return existing ?? undefined; if (!existing) return inputToOnboardingDeploymentInfo(newInfo); @@ -111,7 +111,7 @@ function mergeDeploymentInfo( ? serial === null ? null : serial - : (existingWifi.serial ?? null), + : existingWifi.serial ?? null, }, }; } @@ -161,7 +161,7 @@ export const updateHotspotInfo = // Check wallet has sufficient balance for transaction fees const walletBalance = await connection.getBalance( - new PublicKey(walletAddress), + new PublicKey(walletAddress) ); const required = calculateRequiredBalance(BASE_TX_FEE_LAMPORTS, 0); if (walletBalance < required) { @@ -174,11 +174,11 @@ export const updateHotspotInfo = const hemProgram = await initHemLocal(provider); const [keyToAssetK] = keyToAssetKey(HNT_DAO, entityPubKey); const keyToAsset = await (hemProgram.account as any).keyToAssetV0.fetch( - keyToAssetK, + keyToAssetK ); const entityKey = decodeEntityKey( keyToAsset.entityKey, - keyToAsset.keySerialization, + keyToAsset.keySerialization ); if (!entityKey) { @@ -235,7 +235,7 @@ export const updateHotspotInfo = // Merge existing with new deploymentInfo const mergedDeploymentInfo = mergeDeploymentInfo( existingDeploymentInfo, - input.deploymentInfo, + input.deploymentInfo ); const response = await onboardingClient.updateMobileMetadata({ @@ -289,6 +289,7 @@ export const updateHotspotInfo = hotspotKey: entityPubKey, deviceType: input.deviceType, ...(location && { location }), + ...(h3 && { h3Index: h3.mobile ?? h3.iot }), ...(input.deviceType === "iot" && input.gain !== undefined && { gain: input.gain }), ...(input.deviceType === "iot" && @@ -296,14 +297,24 @@ export const updateHotspotInfo = ...(input.deviceType === "mobile" && input.deploymentInfo && { deploymentType: input.deploymentInfo.type, + ...(input.deploymentInfo.type === "WIFI" && { + antenna: input.deploymentInfo.antenna, + elevation: input.deploymentInfo.elevation, + azimuth: input.deploymentInfo.azimuth, + mechanicalDownTilt: input.deploymentInfo.mechanicalDownTilt, + electricalDownTilt: input.deploymentInfo.electricalDownTilt, + }), + ...(input.deploymentInfo.type === "CBRS" && { + radioInfos: input.deploymentInfo.radioInfos, + }), }), }, }, estimatedSolFee: toTokenAmountOutput( new BN(totalFee), - NATIVE_MINT.toBase58(), + NATIVE_MINT.toBase58() ), appliedTo, }; - }, + } );