Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ CLAUDE.local.md
# hardhat generated files in workspace contract projects
contracts/*/artifacts
contracts/*/cache

.mcp.json
12 changes: 0 additions & 12 deletions .mcp.json

This file was deleted.

2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@polkadot/util-crypto": "catalog:",
"@scure/bip39": "^1.5.4",
"@supabase/supabase-js": "catalog:",
"@types/multer": "^2.1.0",
"@vortexfi/shared": "workspace:*",
"@wagmi/core": "catalog:",
"axios": "catalog:",
Expand All @@ -34,6 +35,7 @@
"joi": "^17.13.3",
"method-override": "^3.0.0",
"morgan": "^1.8.1",
"multer": "^2.1.1",
"node-cache": "^5.1.2",
"p-limit": "^6.1.0",
"pg": "^8.14.1",
Expand Down
199 changes: 188 additions & 11 deletions apps/api/src/api/controllers/alfredpay.controller.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import {
AlfredPayCountry,
AlfredPayStatus,
AlfredpayAddFiatAccountRequest,
AlfredpayApiService,
AlfredpayCreateCustomerRequest,
AlfredpayCreateCustomerResponse,
AlfredpayCustomerType,
AlfredpayFiatAccountType,
AlfredpayGetKybRedirectLinkResponse,
AlfredpayGetKycRedirectLinkRequest,
AlfredpayGetKycRedirectLinkResponse,
AlfredpayGetKycStatusResponse,
AlfredpayKybStatus,
AlfredpayKycFileType,
AlfredpayKycStatus,
AlfredpayStatusRequest,
AlfredpayStatusResponse
AlfredpayStatusResponse,
SubmitKycInformationRequest
} from "@vortexfi/shared";
import { Request, Response } from "express";
import logger from "../../config/logger";
import AlfredPayCustomer from "../../models/alfredPayCustomer.model";
import { SupabaseAuthService } from "../services/auth/supabase.service";

export class AlfredpayController {
private static mapKycStatus(status: AlfredpayKycStatus): AlfredPayStatus | null {
Expand Down Expand Up @@ -96,10 +99,10 @@ export class AlfredpayController {
try {
const { country } = req.body as AlfredpayCreateCustomerRequest;
const userId = req.userId!;
const userEmail = req.userEmail;

const user = await SupabaseAuthService.getUserProfile(userId);
if (!user || !user.email) {
return res.status(404).json({ error: "User not found or email missing" });
if (!userEmail) {
return res.status(400).json({ error: "User email not available" });
}

// Check if customer already exists in our DB
Expand All @@ -113,7 +116,7 @@ export class AlfredpayController {

const alfredpayService = AlfredpayApiService.getInstance();

const newCustomer = await alfredpayService.createCustomer(user.email, AlfredpayCustomerType.INDIVIDUAL, country);
const newCustomer = await alfredpayService.createCustomer(userEmail, AlfredpayCustomerType.INDIVIDUAL, country);
const customerId = newCustomer.customerId;

await AlfredPayCustomer.create({
Expand All @@ -131,7 +134,8 @@ export class AlfredpayController {
res.json(response);
} catch (error) {
logger.error("Error creating Alfredpay customer:", error);
res.status(500).json({ error: "Internal server error" });
const message = error instanceof Error ? error.message : "Internal server error";
res.status(500).json({ error: message });
}
}

Expand Down Expand Up @@ -338,10 +342,10 @@ export class AlfredpayController {
try {
const { country } = req.body as { country: string };
const userId = req.userId!;
const userEmail = req.userEmail;

const user = await SupabaseAuthService.getUserProfile(userId);
if (!user || !user.email) {
return res.status(404).json({ error: "User not found or email missing" });
if (!userEmail) {
return res.status(400).json({ error: "User email not available" });
}

const type = AlfredpayCustomerType.BUSINESS;
Expand All @@ -356,7 +360,7 @@ export class AlfredpayController {

const alfredpayService = AlfredpayApiService.getInstance();

const newCustomer = await alfredpayService.createCustomer(user.email, type, country);
const newCustomer = await alfredpayService.createCustomer(userEmail, type, country);
const customerId = newCustomer.customerId;

await AlfredPayCustomer.create({
Expand Down Expand Up @@ -417,4 +421,177 @@ export class AlfredpayController {
res.status(500).json({ error: "Internal server error" });
}
}

static async submitKycInformation(req: Request, res: Response) {
try {
const { country, ...kycData } = req.body as SubmitKycInformationRequest & { country: string };
const userId = req.userId!;

const alfredPayCustomer = await AlfredPayCustomer.findOne({
where: { country: country as AlfredPayCountry, type: AlfredpayCustomerType.INDIVIDUAL, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay customer not found" });
}

const alfredpayService = AlfredpayApiService.getInstance();
const result = await alfredpayService.submitKycInformation(alfredPayCustomer.alfredPayId, { ...kycData, country });

res.json(result);
} catch (error) {
logger.error("Error submitting KYC information:", error);
const message = error instanceof Error ? error.message : "Internal server error";
res.status(500).json({ error: message });
}
}

static async submitKycFile(req: Request, res: Response) {
try {
const { country, submissionId, fileType } = req.body as { country: string; submissionId: string; fileType: string };
const userId = req.userId!;

if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}

const alfredPayCustomer = await AlfredPayCustomer.findOne({
where: { country: country as AlfredPayCountry, type: AlfredpayCustomerType.INDIVIDUAL, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay customer not found" });
}

const fileBlob = new File([req.file.buffer], req.file.originalname, { type: req.file.mimetype });
const alfredpayService = AlfredpayApiService.getInstance();
await alfredpayService.submitKycFile(
alfredPayCustomer.alfredPayId,
submissionId,
fileType as AlfredpayKycFileType,
fileBlob
);

res.json({ success: true });
} catch (error) {
logger.error("Error submitting KYC file:", error);
const message = error instanceof Error ? error.message : "Internal server error";
res.status(500).json({ error: message });
}
}

static async sendKycSubmission(req: Request, res: Response) {
try {
const { country, submissionId } = req.body as { country: string; submissionId: string };
const userId = req.userId!;

const alfredPayCustomer = await AlfredPayCustomer.findOne({
where: { country: country as AlfredPayCountry, type: AlfredpayCustomerType.INDIVIDUAL, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay customer not found" });
}

const alfredpayService = AlfredpayApiService.getInstance();
await alfredpayService.sendKycSubmission(alfredPayCustomer.alfredPayId, submissionId);

res.json({ success: true });
} catch (error) {
logger.error("Error sending KYC submission:", error);
const message = error instanceof Error ? error.message : "Internal server error";
res.status(500).json({ error: message });
}
}

static async addFiatAccount(req: Request, res: Response) {
try {
const {
country,
type,
accountNumber,
accountType,
accountName,
accountBankCode,
accountAlias,
routingNumber,
networkIdentifier
} = req.body as AlfredpayAddFiatAccountRequest;
const userId = req.userId!;

const alfredPayCustomer = await AlfredPayCustomer.findOne({
order: [["updatedAt", "DESC"]],
where: { country: country as AlfredPayCountry, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay customer not found" });
}

const alfredpayService = AlfredpayApiService.getInstance();
const result = await alfredpayService.createFiatAccount(alfredPayCustomer.alfredPayId, type as AlfredpayFiatAccountType, {
accountAlias: accountAlias ?? "",
accountBankCode,
accountName,
accountNumber,
accountType: accountType ?? "",
networkIdentifier: networkIdentifier ?? "",
routingNumber
});

res.json(result);
} catch (error) {
logger.error("Error adding fiat account:", error);
res.status(500).json({ error: "Internal server error" });
}
}

static async listFiatAccounts(req: Request, res: Response) {
try {
const { country } = req.query as { country: string };
const userId = req.userId!;

const alfredPayCustomer = await AlfredPayCustomer.findOne({
order: [["updatedAt", "DESC"]],
where: { country: country as AlfredPayCountry, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay customer not found" });
}

const alfredpayService = AlfredpayApiService.getInstance();
const accounts = await alfredpayService.listFiatAccounts(alfredPayCustomer.alfredPayId);

res.json(accounts);
} catch (error) {
logger.error("Error listing fiat accounts:", error);
res.status(500).json({ error: "Internal server error" });
}
}

static async deleteFiatAccount(req: Request, res: Response) {
try {
const { fiatAccountId } = req.params;
const { country } = req.query as { country: string };
const userId = req.userId!;

const alfredPayCustomer = await AlfredPayCustomer.findOne({
order: [["updatedAt", "DESC"]],
where: { country: country as AlfredPayCountry, userId }
});

if (!alfredPayCustomer) {
return res.status(404).json({ error: "Alfredpay customer not found" });
}

const alfredpayService = AlfredpayApiService.getInstance();
await alfredpayService.deleteFiatAccount(fiatAccountId);

res.status(204).send();
} catch (error) {
logger.error("Error deleting fiat account:", error);
res.status(500).json({ error: "Internal server error" });
}
}
}
2 changes: 2 additions & 0 deletions apps/api/src/api/middlewares/supabaseAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare global {
namespace Express {
interface Request {
userId?: string;
userEmail?: string;
}
}
}
Expand Down Expand Up @@ -33,6 +34,7 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
}

req.userId = result.user_id;
req.userEmail = result.email;
next();
} catch (error) {
console.error("Auth middleware error:", error);
Expand Down
12 changes: 12 additions & 0 deletions apps/api/src/api/routes/v1/alfredpay.route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Router } from "express";
import multer from "multer";
import { AlfredpayController } from "../../controllers/alfredpay.controller";
import { validateResultCountry } from "../../middlewares/alfredpay.middleware";
import { requireAuth } from "../../middlewares/supabaseAuth";

const router = Router();
const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 }, storage: multer.memoryStorage() });

router.get("/alfredpayStatus", requireAuth, validateResultCountry, AlfredpayController.alfredpayStatus);
router.post("/createIndividualCustomer", requireAuth, validateResultCountry, AlfredpayController.createIndividualCustomer);
Expand All @@ -15,4 +17,14 @@ router.post("/retryKyc", requireAuth, validateResultCountry, AlfredpayController
router.post("/createBusinessCustomer", requireAuth, validateResultCountry, AlfredpayController.createBusinessCustomer);
router.get("/getKybRedirectLink", requireAuth, validateResultCountry, AlfredpayController.getKybRedirectLink);

// MXN API-based KYC
router.post("/submitKycInformation", requireAuth, validateResultCountry, AlfredpayController.submitKycInformation);
router.post("/submitKycFile", requireAuth, upload.single("file"), validateResultCountry, AlfredpayController.submitKycFile);
router.post("/sendKycSubmission", requireAuth, validateResultCountry, AlfredpayController.sendKycSubmission);

// Fiat accounts (USD + MXN)
router.post("/fiatAccounts", requireAuth, validateResultCountry, AlfredpayController.addFiatAccount);
router.get("/fiatAccounts", requireAuth, validateResultCountry, AlfredpayController.listFiatAccounts);
router.delete("/fiatAccounts/:fiatAccountId", requireAuth, validateResultCountry, AlfredpayController.deleteFiatAccount);

export default router;
2 changes: 2 additions & 0 deletions apps/api/src/api/services/auth/supabase.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export class SupabaseAuthService {
static async verifyToken(accessToken: string): Promise<{
valid: boolean;
user_id?: string;
email?: string;
}> {
const { data, error } = await supabase.auth.getUser(accessToken);

Expand All @@ -151,6 +152,7 @@ export class SupabaseAuthService {
}

return {
email: data.user.email,
user_id: data.user.id,
valid: true
};
Expand Down
21 changes: 20 additions & 1 deletion apps/api/src/api/services/priceFeed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,26 @@ export class PriceFeedService {
return cachedEntry.value;
}

// Check if the currency has a Pendulum representative (Nabla pool).
// Currencies like MXN and COP are TokenType.Fiat with no Pendulum pool — use CoinGecko for those.
let outputTokenPendulumDetails;
try {
outputTokenPendulumDetails = getPendulumDetails(toCurrency);
} catch {
// No Pendulum representative — fall back to CoinGecko using USDC as a USD proxy.
logger.debug(`Cache miss for ${cacheKey}. No Pendulum pool for ${toCurrency}, fetching from CoinGecko.`);
try {
const rate = await this.getCryptoPrice("usd-coin", toCurrency.toLowerCase());
this.fiatExchangeRateCache.set(cacheKey, { expiresAt: now + this.fiatCacheTtlMs, value: rate });
return rate;
} catch (cgError) {
if (cgError instanceof Error) {
logger.error(`Error fetching fiat exchange rate from ${fromCurrency} to ${toCurrency}: ${cgError.message}`);
}
throw cgError;
}
}

logger.debug(`Cache miss for ${cacheKey}. Fetching from Nabla.`);

try {
Expand All @@ -211,7 +231,6 @@ export class PriceFeedService {
// We assume that the exchange rate from axlUSDC to the target currency in the Forex AMM
// resemble the real fiat exchange rate.
const inputTokenPendulumDetails = PENDULUM_USDC_AXL;
const outputTokenPendulumDetails = getPendulumDetails(toCurrency);

// Call getTokenOutAmount to get the exchange rate
const amountOut = await getTokenOutAmount({
Expand Down
Loading
Loading