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
11 changes: 1 addition & 10 deletions comebackhere-backend/src/db/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,11 @@ export async function connectMongo(): Promise<Db> {
await invoices.createIndex({ status: 1 })
await invoices.createIndex({ merchant_address: 1 })
await invoices.createIndex({ status: 1, merchant_address: 1 })
await invoices.createIndex({ created_at: -1 })

const cursors = db.collection<IndexerCursor>("indexer_cursors")
await cursors.createIndex({ _id: 1 }, { unique: true })

const invoices = db.collection<InvoiceRecord>("invoices")
await invoices.createIndex({ invoice_id: 1 }, { unique: true })
await invoices.createIndex({ status: 1 })
await invoices.createIndex({ merchant_address: 1 })
await invoices.createIndex({ created_at: -1 })

return db
}

Expand All @@ -171,10 +166,6 @@ export function getCursorsCollection(database: Db): Collection<IndexerCursor> {
return database.collection<IndexerCursor>("indexer_cursors")
}

export function getInvoicesCollection(database: Db): Collection<InvoiceRecord> {
return database.collection<InvoiceRecord>("invoices")
}

export async function closeMongo(): Promise<void> {
if (client) {
await client.close()
Expand Down
50 changes: 50 additions & 0 deletions comebackhere-backend/src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Response } from "express"
import { getNetworkPassphrase } from "./soroban.js"

/**
* Env object returned by {@link requireEnv} for a route.
*
* `rpcUrl` and `networkPassphrase` are always present. Every additional
* property comes from the `vars` mapping passed to {@link requireEnv}.
*/
export type ContractEnv<P extends Record<string, string>> = {
rpcUrl: string
networkPassphrase: string
} & { [Prop in keyof P]: string }

const MISSING_ENV_ERROR = "Service misconfiguration: missing required environment variables"

/**
* Reads and validates the env vars a route needs from `process.env`.
*
* `SOROBAN_RPC_URL` (returned as `rpcUrl`) and the network passphrase
* (returned as `networkPassphrase`) are always validated. `vars` maps each
* additional property name to the env var it should be read from, e.g.
* `{ treasuryContractId: "TREASURY_CONTRACT_ID" }`.
*
* If any referenced var is unset, writes a 503 with the standard
* misconfiguration error to `res` and returns null.
*/
export function requireEnv<P extends Record<string, string>>(
res: Response,
vars: P,
): ContractEnv<P> | null {
const missing = [
!process.env.SOROBAN_RPC_URL ? "SOROBAN_RPC_URL" : null,
...Object.values(vars).filter((envName) => !process.env[envName]),
].filter(Boolean)
if (missing.length > 0) {
res.status(503).json({ error: MISSING_ENV_ERROR })
return null
}

const values = Object.fromEntries(
Object.entries(vars).map(([prop, envName]) => [prop, process.env[envName] as string]),
) as { [Prop in keyof P]: string }

return {
rpcUrl: process.env.SOROBAN_RPC_URL as string,
networkPassphrase: getNetworkPassphrase(),
...values,
}
}
35 changes: 13 additions & 22 deletions comebackhere-backend/src/routes/compliance.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Router, type Request, type Response } from "express"
import {
Keypair,
Networks,
TransactionBuilder,
BASE_FEE,
Contract,
nativeToScVal,
SorobanRpc,
} from "stellar-sdk"
import { requireEnv } from "../lib/env.js"
import { validateBody } from "../middleware/validate.js"
import { allowBodySchema, blockBodySchema } from "../schemas/index.js"

Expand All @@ -34,15 +34,6 @@ function buildSorobanClient(rpcUrl: string): SorobanClient {
}
}

function envOrError(): { rpcUrl: string; contractId: string; signerSecret: string; networkPassphrase: string } | null {
const rpcUrl = process.env.SOROBAN_RPC_URL
const contractId = process.env.COMPLIANCE_CONTRACT_ID
const signerSecret = process.env.SIGNER_SECRET_KEY
const networkPassphrase = process.env.NETWORK_PASSPHRASE ?? Networks.STANDALONE
if (!rpcUrl || !contractId || !signerSecret) return null
return { rpcUrl, contractId, signerSecret, networkPassphrase }
}

// ---------------------------------------------------------------------------
// Core call — submit a compliance operation and return updated status
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -138,11 +129,11 @@ router.post("/allow", validateBody(allowBodySchema), async (req: Request, res: R

const { address, until } = req.body as { address: string; until?: number }

const env = envOrError()
if (!env) {
res.status(503).json({ error: "Service misconfiguration: missing required environment variables" })
return
}
const env = requireEnv(res, {
complianceContractId: "COMPLIANCE_CONTRACT_ID",
signerSecret: "SIGNER_SECRET_KEY",
})
if (!env) return

try {
const client = buildSorobanClient(env.rpcUrl)
Expand All @@ -155,7 +146,7 @@ router.post("/allow", validateBody(allowBodySchema), async (req: Request, res: R
operation as "allow_address" | "allow_address_until",
args,
client,
env.contractId,
env.complianceContractId,
env.signerSecret,
env.networkPassphrase
)
Expand Down Expand Up @@ -190,11 +181,11 @@ router.post("/block", validateBody(blockBodySchema), async (req: Request, res: R

const { address } = req.body as { address: string }

const env = envOrError()
if (!env) {
res.status(503).json({ error: "Service misconfiguration: missing required environment variables" })
return
}
const env = requireEnv(res, {
complianceContractId: "COMPLIANCE_CONTRACT_ID",
signerSecret: "SIGNER_SECRET_KEY",
})
if (!env) return

// Audit log — admin identity + timestamp
console.log(`[compliance] block_address admin="${adminKey}" address="${address}" ts="${new Date().toISOString()}"`)
Expand All @@ -205,7 +196,7 @@ router.post("/block", validateBody(blockBodySchema), async (req: Request, res: R
"block_address",
[nativeToScVal(address, { type: "address" })],
client,
env.contractId,
env.complianceContractId,
env.signerSecret,
env.networkPassphrase
)
Expand Down
10 changes: 2 additions & 8 deletions comebackhere-backend/src/routes/disputes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Router, type Request, type Response } from "express"
import { requireEnv } from "../lib/env.js"
import { validateBody } from "../middleware/validate.js"
import { voteBodySchema, createDisputeSchema } from "../schemas/index.js"

Expand Down Expand Up @@ -208,14 +209,7 @@ export interface CreateDisputeBody {
router.post("/", validateBody(createDisputeSchema), async (req: Request, res: Response) => {
const body = req.body as CreateDisputeBody

const rpcUrl = process.env.SOROBAN_RPC_URL
const settlementContractId = process.env.SETTLEMENT_CONTRACT_ID
const signerSecret = process.env.SIGNER_SECRET_KEY

if (!rpcUrl || !settlementContractId || !signerSecret) {
res.status(503).json({ error: "Service misconfiguration: missing required environment variables" })
return
}
if (!requireEnv(res, { settlementContractId: "SETTLEMENT_CONTRACT_ID", signerSecret: "SIGNER_SECRET_KEY" })) return

const settlementId = body.settlement_id
const claimantAddress = body.claimant_address
Expand Down
33 changes: 9 additions & 24 deletions comebackhere-backend/src/routes/invoice-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,16 @@ import { Router, type Request, type Response } from "express"
import { Keypair, nativeToScVal } from "stellar-sdk"
import {
buildSorobanClient,
getNetworkPassphrase,
simulateContractRead,
submitContractCall,
type SorobanClient,
} from "../lib/soroban.js"
import { requireEnv } from "../lib/env.js"
import { validateBody } from "../middleware/validate.js"
import { graceWindowSchema } from "../schemas/index.js"

const router = Router()

function requireEnv(res: Response): {
rpcUrl: string
invoiceContractId: string
signerSecret: string
networkPassphrase: string
} | null {
const rpcUrl = process.env.SOROBAN_RPC_URL
const invoiceContractId = process.env.INVOICE_CONTRACT_ID
const signerSecret = process.env.SIGNER_SECRET_KEY
const networkPassphrase = getNetworkPassphrase()

if (!rpcUrl || !invoiceContractId || !signerSecret) {
res.status(503).json({
error: "Service misconfiguration: missing required environment variables",
})
return null
}

return { rpcUrl, invoiceContractId, signerSecret, networkPassphrase }
}

/**
* @openapi
* /api/invoice/grace-window:
Expand All @@ -58,7 +37,10 @@ function requireEnv(res: Response): {
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get("/grace-window", async (_req: Request, res: Response) => {
const env = requireEnv(res)
const env = requireEnv(res, {
invoiceContractId: "INVOICE_CONTRACT_ID",
signerSecret: "SIGNER_SECRET_KEY",
})
if (!env) return

try {
Expand Down Expand Up @@ -161,7 +143,10 @@ export async function setGraceWindow(
* $ref: '#/components/schemas/ErrorResponse'
*/
router.post("/grace-window", validateBody(graceWindowSchema), async (req: Request, res: Response) => {
const env = requireEnv(res)
const env = requireEnv(res, {
invoiceContractId: "INVOICE_CONTRACT_ID",
signerSecret: "SIGNER_SECRET_KEY",
})
if (!env) return

const graceWindowSeconds = req.body.grace_window_seconds
Expand Down
43 changes: 18 additions & 25 deletions comebackhere-backend/src/routes/invoices.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Router, type Request, type Response } from "express"
import { Keypair, Networks, TransactionBuilder, BASE_FEE, Contract, nativeToScVal, SorobanRpc, xdr } from "stellar-sdk"
import { Keypair, TransactionBuilder, BASE_FEE, Contract, nativeToScVal, SorobanRpc, xdr } from "stellar-sdk"
import { connectMongo, getInvoicesCollection, type InvoiceRecord, type InvoiceStatus } from "../db/mongo.js"
import { requireEnv } from "../lib/env.js"
import { cacheGet, cacheSet } from "../lib/cache.js"
import { validateBody, validateParams } from "../middleware/validate.js"
import { createInvoiceSchema, invoiceIdParamSchema } from "../schemas/index.js"

const router = Router()

Expand Down Expand Up @@ -249,18 +252,12 @@ router.get("/", async (req: Request, res: Response) => {
router.get("/:id", validateParams(invoiceIdParamSchema), async (req: Request, res: Response) => {
const { id } = req.params

const rpcUrl = process.env.SOROBAN_RPC_URL
const contractId = process.env.INVOICE_CONTRACT_ID
const networkPassphrase = process.env.NETWORK_PASSPHRASE ?? Networks.STANDALONE

if (!rpcUrl || !contractId) {
res.status(503).json({ error: "Service misconfiguration: missing required environment variables" })
return
}
const env = requireEnv(res, { invoiceContractId: "INVOICE_CONTRACT_ID" })
if (!env) return

try {
const server = new SorobanRpc.Server(rpcUrl)
const contract = new Contract(contractId)
const server = new SorobanRpc.Server(env.rpcUrl)
const contract = new Contract(env.invoiceContractId)

// Build a read-only ledger entry query for the invoice
const ledgerKey = contract.getFootprint()
Expand All @@ -270,7 +267,7 @@ router.get("/:id", validateParams(invoiceIdParamSchema), async (req: Request, re
const entries = await server.getLedgerEntries(
xdr.LedgerKey.contractData(
new xdr.LedgerKeyContractData({
contract: new Contract(contractId).address().toScAddress(),
contract: new Contract(env.invoiceContractId).address().toScAddress(),
key: nativeToScVal(BigInt(id), { type: "u64" }),
durability: xdr.ContractDataDurability.persistent(),
})
Expand Down Expand Up @@ -358,24 +355,20 @@ router.get("/:id", validateParams(invoiceIdParamSchema), async (req: Request, re
* $ref: '#/components/schemas/ErrorResponse'
*/
router.post("/", validateBody(createInvoiceSchema), async (req: Request, res: Response) => {
const rpcUrl = process.env.SOROBAN_RPC_URL
const contractId = process.env.INVOICE_CONTRACT_ID
const signerSecret = process.env.SIGNER_SECRET_KEY
const networkPassphrase = process.env.NETWORK_PASSPHRASE ?? Networks.STANDALONE

if (!rpcUrl || !contractId || !signerSecret) {
res.status(503).json({ error: "Service misconfiguration: missing required environment variables" })
return
}
const env = requireEnv(res, {
invoiceContractId: "INVOICE_CONTRACT_ID",
signerSecret: "SIGNER_SECRET_KEY",
})
if (!env) return

try {
const client = buildSorobanClient(rpcUrl)
const client = buildSorobanClient(env.rpcUrl)
const result = await createInvoice(
req.body as CreateInvoiceBody,
client,
contractId,
signerSecret,
networkPassphrase
env.invoiceContractId,
env.signerSecret,
env.networkPassphrase
)

const db = await connectMongo()
Expand Down
28 changes: 5 additions & 23 deletions comebackhere-backend/src/routes/release-escrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,15 @@ import { Router, type Request, type Response } from "express"
import { Keypair, nativeToScVal } from "stellar-sdk"
import {
buildSorobanClient,
getNetworkPassphrase,
submitContractCall,
type SorobanClient,
} from "../lib/soroban.js"
import { requireEnv } from "../lib/env.js"
import { validateBody, validateParams } from "../middleware/validate.js"
import { releaseEscrowIdParamSchema } from "../schemas/index.js"

const router = Router({ mergeParams: true })

function requireEnv(res: Response): {
rpcUrl: string
invoiceContractId: string
signerSecret: string
networkPassphrase: string
} | null {
const rpcUrl = process.env.SOROBAN_RPC_URL
const invoiceContractId = process.env.INVOICE_CONTRACT_ID
const signerSecret = process.env.SIGNER_SECRET_KEY
const networkPassphrase = getNetworkPassphrase()

if (!rpcUrl || !invoiceContractId || !signerSecret) {
res.status(503).json({
error: "Service misconfiguration: missing required environment variables",
})
return null
}

return { rpcUrl, invoiceContractId, signerSecret, networkPassphrase }
}

export interface ReleaseEscrowResult {
invoice_id: number
status: "Released"
Expand Down Expand Up @@ -102,7 +81,10 @@ router.post("/:id/release-escrow", validateParams(releaseEscrowIdParamSchema), a
const { id } = req.params

const invoiceId = parseInt(id, 10)
const env = requireEnv(res)
const env = requireEnv(res, {
invoiceContractId: "INVOICE_CONTRACT_ID",
signerSecret: "SIGNER_SECRET_KEY",
})
if (!env) return

try {
Expand Down
Loading
Loading