- {Number(invoice.amount).toFixed(2)}
+ {Number(invoice.amount).toString()}
{formatCurrencyLabel(invoice.invoiceCurrency)}
diff --git a/src/components/app-kit.tsx b/src/components/app-kit.tsx
index d7b141bb..5a323256 100644
--- a/src/components/app-kit.tsx
+++ b/src/components/app-kit.tsx
@@ -1,7 +1,14 @@
"use client";
import { Ethers5Adapter } from "@reown/appkit-adapter-ethers5";
-import { sepolia } from "@reown/appkit/networks";
+import {
+ arbitrum,
+ base,
+ mainnet,
+ optimism,
+ polygon,
+ sepolia,
+} from "@reown/appkit/networks";
import { createAppKit } from "@reown/appkit/react";
const metadata = {
@@ -20,7 +27,7 @@ if (!projectId) {
createAppKit({
adapters: [new Ethers5Adapter()],
metadata,
- networks: [sepolia],
+ networks: [sepolia, base, mainnet, arbitrum, optimism, polygon],
projectId,
features: {
analytics: false,
diff --git a/src/components/invoice-form.tsx b/src/components/invoice-form.tsx
index 67d1286c..71817efd 100644
--- a/src/components/invoice-form.tsx
+++ b/src/components/invoice-form.tsx
@@ -15,13 +15,16 @@ import { Textarea } from "@/components/ui/textarea";
import {
INVOICE_CURRENCIES,
type InvoiceCurrency,
+ MAINNET_CURRENCIES,
+ type MainnetCurrency,
formatCurrencyLabel,
getPaymentCurrenciesForInvoice,
} from "@/lib/currencies";
import type { InvoiceFormValues } from "@/lib/schemas/invoice";
-import { Plus, Trash2 } from "lucide-react";
+import { Plus, Terminal, Trash2 } from "lucide-react";
import type { UseFormReturn } from "react-hook-form";
import { useFieldArray } from "react-hook-form";
+import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
type RecurringFrequency = "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
@@ -223,6 +226,8 @@ export function InvoiceForm({
valueAsNumber: true,
})}
type="number"
+ step="any"
+ min="0"
placeholder="Price"
/>
{form.formState.errors.items?.[index]?.price && (
@@ -294,6 +299,33 @@ export function InvoiceForm({
)}
+ {MAINNET_CURRENCIES.includes(
+ form.watch("invoiceCurrency") as MainnetCurrency,
+ ) && (
+
+
+
+
+ Warning: You are creating an invoice with real funds
+
+
+
+ You've selected{" "}
+
+ {formatCurrencyLabel(form.watch("invoiceCurrency"))}
+
+ , which operates on a mainnet blockchain and uses real
+ cryptocurrency.
+
+
+ EasyInvoice is a demonstration app only, designed to showcase
+ Request Network API functionality. Do not use for real
+ invoicing.
+
+
+
+
+ )}
{/* Only show payment currency selector for USD invoices */}
{form.watch("invoiceCurrency") === "USD" && (
diff --git a/src/components/invoice-preview.tsx b/src/components/invoice-preview.tsx
index 7cf08bf0..96bb202a 100644
--- a/src/components/invoice-preview.tsx
+++ b/src/components/invoice-preview.tsx
@@ -128,10 +128,10 @@ export function InvoicePreview({ data }: InvoicePreviewProps) {
{item.quantity}
- {(item.price || 0).toFixed(2)}
+ {(item.price || 0).toString()}
- {((item.quantity || 0) * (item.price || 0)).toFixed(2)}
+ {((item.quantity || 0) * (item.price || 0)).toString()}
))}
@@ -143,13 +143,13 @@ export function InvoicePreview({ data }: InvoicePreviewProps) {
Subtotal
- {calculateTotal().toFixed(2)}
+ {calculateTotal().toString()}
Total
- {calculateTotal().toFixed(2)}
+ {calculateTotal().toString()}
{formatCurrencyLabel(data.invoiceCurrency || "USD")}
diff --git a/src/components/payment-route.tsx b/src/components/payment-route.tsx
new file mode 100644
index 00000000..2b9b3a90
--- /dev/null
+++ b/src/components/payment-route.tsx
@@ -0,0 +1,117 @@
+import type { PaymentRoute as PaymentRouteType } from "@/lib/types";
+import { ArrowRight, Globe, Zap } from "lucide-react";
+import Image from "next/image";
+
+interface RouteTypeInfo {
+ type: "direct" | "same-chain-erc20" | "cross-chain";
+ label: string;
+ description: string;
+}
+
+interface PaymentRouteProps {
+ route: PaymentRouteType;
+ isSelected: boolean;
+ onClick?: () => void;
+ variant?: "default" | "selected";
+ routeType?: RouteTypeInfo;
+}
+
+export function PaymentRoute({
+ route,
+ isSelected,
+ onClick,
+ variant = "default",
+ routeType,
+}: PaymentRouteProps) {
+ const isDirectPayment =
+ routeType?.type === "direct" || route.id === "REQUEST_NETWORK_PAYMENT";
+ const isGasFreePayment = routeType?.type === "same-chain-erc20";
+
+ const nativeToken = route.chain === "POLYGON" ? "POL" : "ETH";
+
+ // Get the appropriate badge color and icon based on route type
+ const getBadgeStyles = () => {
+ if (isDirectPayment) {
+ return {
+ bgColor: "bg-blue-100",
+ textColor: "text-blue-700",
+ icon:
,
+ };
+ }
+
+ if (isGasFreePayment) {
+ return {
+ bgColor: "bg-green-100",
+ textColor: "text-green-700",
+ icon:
,
+ };
+ }
+
+ return {
+ bgColor: "bg-purple-100",
+ textColor: "text-purple-700",
+ icon:
,
+ };
+ };
+
+ const { bgColor, textColor, icon } = getBadgeStyles();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {route.chain} {route.token}
+
+
+ {icon}
+ {routeType?.label ||
+ (isDirectPayment ? "Direct Payment" : "via Paygrid")}
+
+
+
+ {routeType?.description || `via ${route.token}`}
+
+
+
+
+
+ {route.fee === 0 ? (
+ "No fee"
+ ) : (
+
+ {route.fee} {isDirectPayment ? nativeToken : route.token} fee
+
+ )}
+
+
+ {typeof route.speed === "number" ? `~${route.speed}s` : "Fast"}
+
+
+
+
+ );
+}
diff --git a/src/components/payment-section.tsx b/src/components/payment-section.tsx
index 3a3f3a62..61846de8 100644
--- a/src/components/payment-section.tsx
+++ b/src/components/payment-section.tsx
@@ -1,18 +1,23 @@
"use client";
+import { PaymentRoute } from "@/components/payment-route";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
-import { formatCurrencyLabel } from "@/lib/currencies";
+import { CHAIN_TO_ID, ID_TO_APPKIT_NETWORK } from "@/lib/chains";
+import { MAINNET_CURRENCIES, formatCurrencyLabel } from "@/lib/currencies";
+import type { PaymentRoute as PaymentRouteType } from "@/lib/types";
import type { Request } from "@/server/db/schema";
import { api } from "@/trpc/react";
import {
useAppKit,
useAppKitAccount,
+ useAppKitNetwork,
useAppKitProvider,
} from "@reown/appkit/react";
import { ethers } from "ethers";
-import { CheckCircle, Clock, Loader2, Wallet } from "lucide-react";
+import { AlertCircle, CheckCircle, Clock, Loader2, Wallet } from "lucide-react";
+
import { useEffect, useState } from "react";
import { toast } from "sonner";
@@ -20,16 +25,87 @@ interface PaymentSectionProps {
invoice: NonNullable
;
}
+const getCurrencyChain = (currency: string) => {
+ // Extract chain from format like "USDC-base"
+ const parts = currency.split("-");
+ return parts.length > 1 ? parts[1].toLowerCase() : null;
+};
+
+const REQUEST_NETWORK_CHAIN_TO_PAYMENT_NETWORK = {
+ matic: "polygon",
+ base: "base",
+ "arbitrum-one": "arbitrum",
+ optimism: "optimism",
+ mainnet: "ethereum",
+};
+
+const getRouteType = (route: PaymentRouteType, invoiceChain: string | null) => {
+ const invoiceChainToUse =
+ invoiceChain &&
+ REQUEST_NETWORK_CHAIN_TO_PAYMENT_NETWORK[
+ invoiceChain as keyof typeof REQUEST_NETWORK_CHAIN_TO_PAYMENT_NETWORK
+ ];
+
+ if (route.id === "REQUEST_NETWORK_PAYMENT") {
+ return {
+ type: "direct" as const,
+ label: "Direct Payment",
+ description: "Pay directly on the same network",
+ };
+ }
+
+ if (
+ route.chain &&
+ invoiceChainToUse &&
+ route.chain.toLowerCase() === invoiceChainToUse
+ ) {
+ return {
+ type: "same-chain-erc20" as const,
+ label: "Same-Chain ERC20",
+ description: `Pay with ${route.token} (no gas token needed)`,
+ };
+ }
+
+ return {
+ type: "cross-chain" as const,
+ label: "Cross-Chain Payment",
+ description: `Pay from ${route.chain} network using ${route.token}`,
+ };
+};
+
export function PaymentSection({ invoice }: PaymentSectionProps) {
+ const { open } = useAppKit();
+ const { isConnected, address } = useAppKitAccount();
+ const { walletProvider } = useAppKitProvider("eip155");
+ const { chainId, switchNetwork } = useAppKitNetwork();
+ const [showRoutes, setShowRoutes] = useState(false);
+ const [selectedRoute, setSelectedRoute] = useState(
+ null,
+ );
+
const [paymentStatus, setPaymentStatus] = useState(invoice.status);
const [paymentProgress, setPaymentProgress] = useState("idle");
const [currentStep, setCurrentStep] = useState(1);
const [isAppKitReady, setIsAppKitReady] = useState(false);
const { mutateAsync: payRequest } = api.invoice.payRequest.useMutation();
+ const { mutateAsync: sendPaymentIntent } =
+ api.invoice.sendPaymentIntent.useMutation();
+ const {
+ data: paymentRoutes,
+ refetch,
+ isLoading: isLoadingPaymentRoutes,
+ } = api.invoice.getPaymentRoutes.useQuery(
+ {
+ paymentReference: invoice.paymentReference,
+ walletAddress: address as string,
+ },
+ {
+ enabled: !!address,
+ },
+ );
- const { open } = useAppKit();
- const { isConnected, address } = useAppKitAccount();
- const { walletProvider } = useAppKitProvider("eip155");
+ // Extract the chain from invoice currency
+ const invoiceChain = getCurrencyChain(invoice.paymentCurrency);
const displayPaymentProgress = () => {
switch (paymentProgress) {
@@ -57,74 +133,185 @@ export function PaymentSection({ invoice }: PaymentSectionProps) {
setCurrentStep(isConnected ? 2 : 1);
}, [isConnected]);
+ useEffect(() => {
+ if (address) {
+ refetch();
+ }
+ }, [address, refetch]);
+
+ useEffect(() => {
+ if (paymentRoutes) {
+ setSelectedRoute(paymentRoutes[0]);
+ }
+ }, [paymentRoutes]);
+
const handleConnectWallet = () => {
if (isAppKitReady) {
open();
}
};
- const handlePayment = async () => {
- if (paymentProgress !== "idle") return;
+ const handlePaygridPayments = async (paymentData: any, signer: any) => {
+ const paymentIntent = JSON.parse(paymentData.paymentIntent);
+ const supportsEIP2612 = paymentData.metadata.supportsEIP2612;
+ let approvalSignature = undefined;
+ let approval = undefined;
- setPaymentProgress("getting-transactions");
+ setPaymentProgress("approving");
- const ethersProvider = new ethers.providers.Web3Provider(
- walletProvider as ethers.providers.ExternalProvider,
+ if (supportsEIP2612) {
+ approval = JSON.parse(paymentData.approvalPermitPayload);
+
+ approvalSignature = await signer._signTypedData(
+ approval.domain,
+ approval.types,
+ approval.values,
+ );
+ } else {
+ const tx = await signer.sendTransaction(paymentData.approvalCalldata);
+ await tx.wait();
+ }
+
+ const paymentIntentSignature = await signer._signTypedData(
+ paymentIntent.domain,
+ paymentIntent.types,
+ paymentIntent.values,
);
- const signer = await ethersProvider.getSigner();
+ const signedPermit = {
+ signedPaymentIntent: {
+ signature: paymentIntentSignature,
+ nonce: paymentIntent.values.nonce.toString(),
+ deadline: paymentIntent.values.deadline.toString(),
+ },
+ signedApprovalPermit: approvalSignature
+ ? {
+ signature: approvalSignature,
+ nonce: approval.values.nonce.toString(),
+ deadline: approval?.values?.deadline
+ ? approval.values.deadline.toString()
+ : approval.values.expiry.toString(),
+ }
+ : undefined,
+ };
+
+ setPaymentProgress("paying");
+
+ await sendPaymentIntent({
+ paymentIntent: paymentData.paymentIntentId,
+ payload: signedPermit,
+ });
+
+ setPaymentStatus("processing");
+
+ toast("Payment is being processed", {
+ description: "You can safely close this page.",
+ });
+ };
- try {
- const paymentData = await payRequest(invoice.paymentReference).then(
- (response) => response.data,
+ const handleDirectPayments = async (paymentData: any, signer: any) => {
+ const isApprovalNeeded = paymentData.metadata.needsApproval;
+
+ if (isApprovalNeeded) {
+ setPaymentProgress("approving");
+ toast("Payment Approval Required", {
+ description: "Please approve the payment in your wallet",
+ });
+
+ const approvalIndex = paymentData.metadata.approvalTransactionIndex;
+
+ const approvalTransaction = await signer.sendTransaction(
+ paymentData.transactions[approvalIndex],
);
- const isApprovalNeeded = paymentData.metadata.needsApproval;
+ await approvalTransaction.wait();
+ }
- if (isApprovalNeeded) {
- setPaymentProgress("approving");
- toast("Payment Approval Required", {
- description: "Please approve the payment in your wallet",
- });
+ setPaymentProgress("paying");
- const approvalIndex = paymentData.metadata.approvalTransactionIndex;
+ toast("Initiating payment", {
+ description: "Please confirm the payment in your wallet",
+ });
- const approvalTransaction = await signer.sendTransaction(
- paymentData.transactions[approvalIndex],
- );
+ const paymentTransaction = await signer.sendTransaction(
+ paymentData.transactions[isApprovalNeeded ? 1 : 0],
+ );
- await approvalTransaction.wait();
- }
+ await paymentTransaction.wait();
- setPaymentProgress("paying");
+ toast("Payment is being processed", {
+ description: "You can safely close this page.",
+ });
- toast("Initiating payment", {
- description: "Please confirm the payment in your wallet",
- });
+ setPaymentStatus("processing");
+ };
- const paymentTransaction = await signer.sendTransaction(
- paymentData.transactions[isApprovalNeeded ? 1 : 0],
- );
+ const handlePayment = async () => {
+ if (paymentProgress !== "idle") return;
+
+ setPaymentProgress("getting-transactions");
+
+ const targetChain =
+ CHAIN_TO_ID[selectedRoute?.chain as keyof typeof CHAIN_TO_ID];
- await paymentTransaction.wait();
+ if (targetChain !== chainId) {
+ const targetAppkitNetwork =
+ ID_TO_APPKIT_NETWORK[targetChain as keyof typeof ID_TO_APPKIT_NETWORK];
- toast("Payment completed", {
- description: "Payment completed successfully",
+ toast("Switching to network", {
+ description: `Switching to ${targetAppkitNetwork.name} network`,
});
- setPaymentStatus("paid");
+ try {
+ await switchNetwork(targetAppkitNetwork);
+ } catch (_) {
+ toast("Error switching network");
+ return;
+ }
+ }
+
+ const ethersProvider = new ethers.providers.Web3Provider(
+ walletProvider as ethers.providers.ExternalProvider,
+ );
+
+ const signer = await ethersProvider.getSigner();
+
+ try {
+ const paymentData = await payRequest({
+ paymentReference: invoice.paymentReference,
+ wallet: address,
+ chain:
+ selectedRoute?.id === "REQUEST_NETWORK_PAYMENT"
+ ? undefined
+ : selectedRoute?.chain,
+ token:
+ selectedRoute?.id === "REQUEST_NETWORK_PAYMENT"
+ ? undefined
+ : selectedRoute?.token,
+ }).then((response) => response.data);
+
+ const isPaygrid = paymentData.paymentIntentId;
+
+ if (isPaygrid) {
+ await handlePaygridPayments(paymentData, signer);
+ } else {
+ await handleDirectPayments(paymentData, signer);
+ }
} catch (error) {
console.error("Error : ", error);
toast("Payment Failed", {
description: "Please try again",
});
+ } finally {
+ setPaymentProgress("idle");
}
- setPaymentProgress("idle");
};
const showCurrencyConversion =
invoice.invoiceCurrency !== invoice.paymentCurrency;
+ const hasRoutes = paymentRoutes && paymentRoutes.length > 0;
+
return (
@@ -135,7 +322,7 @@ export function PaymentSection({ invoice }: PaymentSectionProps) {
paymentStatus === "paid"
? "bg-green-100 text-green-800"
: paymentStatus === "processing"
- ? "bg-yellow-100 text-yellow-800"
+ ? "bg-orange-100 text-orange-800"
: "bg-blue-100 text-blue-800"
}`}
>
@@ -153,6 +340,7 @@ export function PaymentSection({ invoice }: PaymentSectionProps) {
+ {/* Secure Payment Section */}
Secure Payment
@@ -161,12 +349,31 @@ export function PaymentSection({ invoice }: PaymentSectionProps) {
+ {MAINNET_CURRENCIES.includes(invoice.invoiceCurrency as any) && (
+
+
+
+ Real Cryptocurrency Warning
+
+
+ This invoice uses{" "}
+
+ {formatCurrencyLabel(invoice.invoiceCurrency)}
+ {" "}
+ on a mainnet blockchain, which means you'll be transferring{" "}
+ actual value . EasyInvoice is a
+ demonstration app and all blockchain transactions are
+ irreversible.
+
+
+ )}
+
Amount Due
{formatCurrencyLabel(invoice.invoiceCurrency)}{" "}
- {Number(invoice.amount).toFixed(2)}
+ {Number(invoice.amount).toString()}
{showCurrencyConversion && (
@@ -288,15 +495,107 @@ export function PaymentSection({ invoice }: PaymentSectionProps) {
invoice.paymentCurrency,
)}.`}
+
+ {/* Payment Route Selection */}
+
+
+ Payment Route
+ {paymentRoutes && paymentRoutes.length > 1 && (
+ setShowRoutes(!showRoutes)}
+ >
+ {showRoutes ? "Hide Options" : "See Other Options"}
+
+ )}
+
+
+ {isLoadingPaymentRoutes ? (
+
+
+
+
+
+
+ Loading Payment Routes
+
+
+ Finding the best payment routes for your wallet
+
+
+
+ ) : !paymentRoutes || paymentRoutes.length === 0 ? (
+
+
+
+
+ No Payment Routes Available
+
+
+ There are currently no available payment routes for
+ this transaction.
+
+
+
+ ) : (
+ <>
+ {/* Selected Route Preview */}
+ {selectedRoute && (
+
+ )}
+
+ {/* Route Options - Only show if there are multiple routes */}
+ {showRoutes && paymentRoutes.length > 1 && (
+
+
+ Available Payment Routes
+
+ {paymentRoutes.map((route: PaymentRouteType) => (
+
setSelectedRoute(route)}
+ routeType={getRouteType(route, invoiceChain)}
+ />
+ ))}
+
+ )}
+ >
+ )}
+
+
+ {/* Payment button should be disabled if there are no routes */}
- {paymentProgress !== "idle" && (
-
+ {!hasRoutes ? (
+ "No payment routes available"
+ ) : paymentProgress !== "idle" ? (
+ <>
+
+ {displayPaymentProgress()}
+ >
+ ) : (
+ displayPaymentProgress()
)}
- {displayPaymentProgress()}
)}
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 00000000..b60dd4af
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,61 @@
+import { type VariantProps, cva } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ warning:
+ "border-orange-500/50 text-orange-500 dark:border-orange-500 [&>svg]:text-orange-500",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
& VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/src/lib/chains.ts b/src/lib/chains.ts
new file mode 100644
index 00000000..b36a4f19
--- /dev/null
+++ b/src/lib/chains.ts
@@ -0,0 +1,26 @@
+import {
+ arbitrum,
+ base,
+ mainnet,
+ optimism,
+ polygon,
+ sepolia,
+} from "@reown/appkit/networks";
+
+export const CHAIN_TO_ID = {
+ SEPOLIA: 11155111,
+ BASE: 8453,
+ ETHEREUM: 1,
+ ARBITRUM: 42161,
+ OPTIMISM: 10,
+ POLYGON: 137,
+};
+
+export const ID_TO_APPKIT_NETWORK = {
+ [sepolia.id]: sepolia,
+ [base.id]: base,
+ [mainnet.id]: mainnet,
+ [arbitrum.id]: arbitrum,
+ [optimism.id]: optimism,
+ [polygon.id]: polygon,
+};
diff --git a/src/lib/currencies.ts b/src/lib/currencies.ts
index 3230d4fe..8babb9d4 100644
--- a/src/lib/currencies.ts
+++ b/src/lib/currencies.ts
@@ -1,27 +1,49 @@
+export const MAINNET_CURRENCIES = [
+ "USDC-base",
+ "USDC-optimism",
+ "USDC-matic",
+ "USDC-mainnet",
+ "USDT-mainnet",
+ "USDT-matic",
+ "USDT-optimism",
+ "DAI-mainnet",
+ "DAI-matic",
+ "DAI-optimism",
+] as const;
+
+export type MainnetCurrency = (typeof MAINNET_CURRENCIES)[number];
+
export const INVOICE_CURRENCIES = [
"USD",
"ETH-sepolia-sepolia",
"FAU-sepolia",
"fUSDC-sepolia",
"fUSDT-sepolia",
+ ...MAINNET_CURRENCIES,
] as const;
export type InvoiceCurrency = (typeof INVOICE_CURRENCIES)[number];
-export const PAYMENT_CURRENCIES = {
+export const PAYMENT_CURRENCIES: Partial<{
+ [K in InvoiceCurrency]: readonly string[];
+}> = {
USD: ["ETH-sepolia-sepolia", "FAU-sepolia"] as const,
"ETH-sepolia-sepolia": ["ETH-sepolia-sepolia"] as const,
"FAU-sepolia": ["FAU-sepolia"] as const,
"fUSDC-sepolia": ["fUSDC-sepolia"] as const,
"fUSDT-sepolia": ["fUSDT-sepolia"] as const,
+ ...Object.fromEntries(
+ MAINNET_CURRENCIES.map((currency) => [currency, [currency]]),
+ ),
} as const;
-export type PaymentCurrency =
- (typeof PAYMENT_CURRENCIES)[InvoiceCurrency][number];
+export type PaymentCurrency = NonNullable<
+ (typeof PAYMENT_CURRENCIES)[InvoiceCurrency]
+>[number];
export function getPaymentCurrenciesForInvoice(
invoiceCurrency: InvoiceCurrency,
): PaymentCurrency[] {
- return [...PAYMENT_CURRENCIES[invoiceCurrency]];
+ return [...(PAYMENT_CURRENCIES[invoiceCurrency] || [])];
}
export function formatCurrencyLabel(currency: string): string {
@@ -36,6 +58,26 @@ export function formatCurrencyLabel(currency: string): string {
return "Sepolia USDT";
case "USD":
return "US Dollar";
+ case "USDC-base":
+ return "USDC (Base)";
+ case "USDC-optimism":
+ return "USDC (Optimism)";
+ case "USDC-matic":
+ return "USDC (Polygon)";
+ case "USDC-mainnet":
+ return "USDC (Mainnet)";
+ case "USDT-mainnet":
+ return "USDT (Mainnet)";
+ case "USDT-optimism":
+ return "USDT (Optimism)";
+ case "USDT-matic":
+ return "USDT (Polygon)";
+ case "DAI-mainnet":
+ return "DAI (Mainnet)";
+ case "DAI-optimism":
+ return "DAI (Optimism)";
+ case "DAI-matic":
+ return "DAI (Polygon)";
default:
return currency;
}
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
new file mode 100644
index 00000000..828914b6
--- /dev/null
+++ b/src/lib/types/index.ts
@@ -0,0 +1,8 @@
+export interface PaymentRoute {
+ id: string;
+ fee: number;
+ speed: number | "FAST";
+ price_impact: number;
+ chain: string;
+ token: string;
+}
diff --git a/src/server/routers/invoice.ts b/src/server/routers/invoice.ts
index 41a1a5f3..55409568 100644
--- a/src/server/routers/invoice.ts
+++ b/src/server/routers/invoice.ts
@@ -4,6 +4,7 @@ import { requestTable, userTable } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, isNull, not, or } from "drizzle-orm";
import { ulid } from "ulid";
+import { isEthereumAddress } from "validator";
import { z } from "zod";
import { protectedProcedure, publicProcedure, router } from "../trpc";
@@ -189,20 +190,35 @@ export const invoiceRouter = router({
return invoice;
}),
payRequest: publicProcedure
- .input(z.string())
+ .input(
+ z.object({
+ paymentReference: z.string(),
+ wallet: z.string().optional(),
+ chain: z.string().optional(),
+ token: z.string().optional(),
+ }),
+ )
.mutation(async ({ ctx, input }) => {
const { db } = ctx;
const invoice = await db.query.requestTable.findFirst({
- where: eq(requestTable.paymentReference, input),
+ where: eq(requestTable.paymentReference, input.paymentReference),
});
if (!invoice) {
return { success: false, message: "Invoice not found" };
}
- const response = await apiClient.get(
- `/v1/request/${invoice.paymentReference}/pay`,
- );
+ let paymentEndpoint = `/v1/request/${invoice.paymentReference}/pay?wallet=${input.wallet}`;
+
+ if (input.chain) {
+ paymentEndpoint += `&chain=${input.chain}`;
+ }
+
+ if (input.token) {
+ paymentEndpoint += `&token=${input.token}`;
+ }
+
+ const response = await apiClient.get(paymentEndpoint);
if (response.status !== 200) {
return {
@@ -249,4 +265,51 @@ export const invoiceRouter = router({
return updatedInvoice[0];
}),
+ getPaymentRoutes: publicProcedure
+ .input(
+ z.object({
+ paymentReference: z.string(),
+ walletAddress: z.string().refine(
+ (val) => {
+ return isEthereumAddress(val);
+ },
+ {
+ message: "Invalid wallet address",
+ },
+ ),
+ }),
+ )
+ .query(async ({ input }) => {
+ const { paymentReference, walletAddress } = input;
+
+ const response = await apiClient.get(
+ `/v1/request/${paymentReference}/routes?wallet=${walletAddress}`,
+ );
+
+ if (response.status !== 200) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Failed to get payment routes",
+ });
+ }
+
+ return response.data.routes;
+ }),
+ sendPaymentIntent: publicProcedure
+ .input(
+ z.object({
+ paymentIntent: z.string(),
+ payload: z.any(),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ const { paymentIntent, payload } = input;
+
+ const response = await apiClient.post(
+ `/v1/request/${paymentIntent}/send`,
+ payload,
+ );
+
+ return response.data;
+ }),
});