From 5d42d281a0a8ad74467dd306de26645d4080c671 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 09:47:11 +0530 Subject: [PATCH 01/20] Use USDC prices in model map --- src/app/page.tsx | 8 ++++++-- src/services/fluxService.ts | 6 +++--- src/services/paymentService.ts | 7 +++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 933555b..4ed1f68 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -43,7 +43,7 @@ const Page: React.FC = (): React.ReactElement => { const handleFluxGeneration = (modelId: string, cost: number) => { return async (prompt: string): Promise => { const type = walletState.type; - if (!walletState.connected || !walletState.publicKey || + if (!walletState.connected || !walletState.publicKey || !walletState.type || (type === 'phantom' && !window.phantom) || (type === 'solflare' && !window.solflare)) { return { error: 'Wallet not connected' } @@ -53,7 +53,7 @@ const Page: React.FC = (): React.ReactElement => { const paymentResult = await processMTMPayment( walletState.publicKey, cost, - walletState.type + walletState.type ) if (!paymentResult.success) { @@ -62,6 +62,10 @@ const Page: React.FC = (): React.ReactElement => { const transactionSignature = paymentResult.transactionSignature; + if (!transactionSignature) { + return { error: 'Transaction signature not found' } + } + // Then generate image with specified model return generateWithFlux(prompt, modelId, transactionSignature) } diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index 7f22c2e..e7698d3 100644 --- a/src/services/fluxService.ts +++ b/src/services/fluxService.ts @@ -16,19 +16,19 @@ export const FLUX_MODELS: FluxModelConfig[] = [ modelId: "fal-ai/flux/schnell", name: "Schnell", description: "Fast meme generator", - cost: 300 + cost: 0.1 }, { modelId: "fal-ai/recraft-v3", name: "Recraft", description: "Advanced meme generator", - cost: 400 + cost: 0.15 }, { modelId: "fal-ai/stable-diffusion-v35-large", name: "Marquee", description: "Best meme generator", - cost: 500 + cost: 0.2 } ] diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index a966b7a..ffd281e 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -1,3 +1,5 @@ +import assert from 'assert'; + import { Connection, PublicKey, Transaction } from '@solana/web3.js' import { TOKEN_PROGRAM_ID, @@ -5,8 +7,13 @@ import { createAssociatedTokenAccountInstruction, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token' + import { WalletType } from './types' +assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); +assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); +assert(process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS, 'PAYMENT_RECEIVER_ADDRESS is required'); + const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; -- 2.45.2 From 36a140734a325d5a3982529ae2f51fc3e57cc769 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 11:20:18 +0530 Subject: [PATCH 02/20] Fetch USDC price in MTM using jupiter API --- src/app/page.tsx | 24 ++++++++++++++++++++++-- src/components/AIServiceCard.tsx | 8 +++++--- src/services/paymentService.ts | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 4ed1f68..ec3d0ce 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,10 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import WalletHeader from '../components/WalletHeader' import AIServiceCard from '../components/AIServiceCard' import { generateWithFlux, FluxGenerationResult, FLUX_MODELS } from '../services/fluxService' -import { processMTMPayment } from '../services/paymentService' +import { getUSDCToMTMQuote, processMTMPayment } from '../services/paymentService' import { connectWallet, disconnectWallet, WalletState } from '../services/walletService' import { WalletType } from '../services/types' @@ -15,6 +15,25 @@ const Page: React.FC = (): React.ReactElement => { type: null }) + const [usdcPrice, setUsdcPrice] = useState(0); + + useEffect(() => { + + const fetchPrice = async () => { + try { + const price = await getUSDCToMTMQuote(); + setUsdcPrice(price); + } catch (error) { + console.error('Failed to fetch price:', error); + } + }; + + fetchPrice(); + const interval = setInterval(fetchPrice, 10000); + + return () => clearInterval(interval); + }, []); + const handleConnect = async (walletType: WalletType): Promise => { try { const newWalletState = await connectWallet(walletType) @@ -100,6 +119,7 @@ const Page: React.FC = (): React.ReactElement => { tokenCost={model.cost} isWalletConnected={walletState.connected} onGenerate={handleFluxGeneration(model.modelId, model.cost)} + mtmPrice={model.cost * usdcPrice} /> ))} diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index c078973..aaf48ca 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -8,6 +8,7 @@ interface AIServiceCardProps { tokenCost: number isWalletConnected: boolean onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }> + mtmPrice: number | null } interface GenerationState { @@ -22,7 +23,8 @@ const AIServiceCard: React.FC = ({ description, tokenCost, isWalletConnected, - onGenerate + onGenerate, + mtmPrice }) => { const [inputText, setInputText] = useState('') const [generationState, setGenerationState] = useState({ @@ -81,7 +83,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {tokenCost} MTM + Cost: {mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM
@@ -105,7 +107,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${tokenCost} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM & Generate`} diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index ffd281e..49956e1 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -19,6 +19,8 @@ const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRES const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL; +const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC mint address + const connection = new Connection( SOLANA_RPC_URL, { @@ -48,6 +50,21 @@ async function findAssociatedTokenAddress( )[0] } +export async function getUSDCToMTMQuote() { + const url = `https://api.jup.ag/price/v2?ids=${USDC_MINT}&vsToken=${MTM_TOKEN_MINT}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch quote: ${response.statusText}`); + } + + const quoteResponse = await response.json(); + const price = quoteResponse['data'][USDC_MINT]['price'] + + return price; +} + interface WalletAdapter { signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }> } -- 2.45.2 From 8b5dbb46fa72c488aa967f1a3cbcd1460721f745 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 17:00:30 +0530 Subject: [PATCH 03/20] Use bigint for handling token price --- src/app/api/flux/route.ts | 11 ++++- src/app/page.tsx | 78 ++++++++++++++++++++------------ src/components/AIServiceCard.tsx | 8 ++-- src/services/fluxService.ts | 6 +-- src/services/paymentService.ts | 12 +++-- src/utils/verifyPayment.ts | 15 +++++- 6 files changed, 84 insertions(+), 46 deletions(-) diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index e7b1000..4e1a52d 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -3,7 +3,8 @@ import { fal } from "@fal-ai/client" import { FLUX_MODELS } from '../../../services/fluxService' import { initializeDataSource } from '../../../data-source' -import { verifyPayment, isSignatureUsed, markSignatureAsUsed } from '../../../utils/verifyPayment' +import { verifyPayment, markSignatureAsUsed } from '../../../utils/verifyPayment' +import { getUSDCToMTMQuote } from '../../../services/paymentService' if (!process.env.FAL_AI_KEY) { throw new Error('FAL_AI_KEY is not configured in environment variables') @@ -46,7 +47,13 @@ export async function POST(req: NextRequest): Promise { ) } - const expectedAmount = model.cost; + const MTMFor1USDC = await getUSDCToMTMQuote(); + + // TODO: Use bn.js for better precision + const scale = BigInt(100); + const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + + const expectedAmount = (scaledCost * MTMFor1USDC) / scale; const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount) if (!isPaymentVerified) { diff --git a/src/app/page.tsx b/src/app/page.tsx index ec3d0ce..e462742 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,14 +15,13 @@ const Page: React.FC = (): React.ReactElement => { type: null }) - const [usdcPrice, setUsdcPrice] = useState(0); + const [MTMFor1USDC, setMTMFor1USDC] = useState(BigInt(0)); useEffect(() => { - const fetchPrice = async () => { try { const price = await getUSDCToMTMQuote(); - setUsdcPrice(price); + setMTMFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); } @@ -55,38 +54,44 @@ const Page: React.FC = (): React.ReactElement => { connected: false, publicKey: null, type: null - }) + }); } - } + }; - const handleFluxGeneration = (modelId: string, cost: number) => { + const handleFluxGeneration = (modelId: string, cost: bigint) => { return async (prompt: string): Promise => { - const type = walletState.type; - if (!walletState.connected || !walletState.publicKey || !walletState.type || + const { connected, publicKey, type } = walletState; + + if (!connected || !publicKey || !type || (type === 'phantom' && !window.phantom) || (type === 'solflare' && !window.solflare)) { - return { error: 'Wallet not connected' } + return { error: 'Wallet not connected' } } - // Process payment first - const paymentResult = await processMTMPayment( - walletState.publicKey, - cost, - walletState.type - ) + try { + // Convert cost in USDC to MTM tokens using the price ratio + const paymentResult = await processMTMPayment( + publicKey, + cost, + type + ) - if (!paymentResult.success) { - return { error: paymentResult.error } + if (!paymentResult.success) { + return { error: paymentResult.error } + } + + const transactionSignature = paymentResult.transactionSignature; + + if (!transactionSignature) { + return { error: 'Transaction signature not found' } + } + + // Generate image with specified model and transaction reference + return generateWithFlux(prompt, modelId, transactionSignature) + } catch (error) { + console.error('Error in handleFluxGeneration:', error) + return { error: error instanceof Error ? error.message : 'Unknown error' } } - - const transactionSignature = paymentResult.transactionSignature; - - if (!transactionSignature) { - return { error: 'Transaction signature not found' } - } - - // Then generate image with specified model - return generateWithFlux(prompt, modelId, transactionSignature) } } @@ -116,11 +121,24 @@ const Page: React.FC = (): React.ReactElement => { key={model.modelId} title={model.name} description={model.description} - tokenCost={model.cost} isWalletConnected={walletState.connected} - onGenerate={handleFluxGeneration(model.modelId, model.cost)} - mtmPrice={model.cost * usdcPrice} - /> + // TODO: Use bn.js for better precision + onGenerate={handleFluxGeneration( + model.modelId, + (() => { + const scale = BigInt(100); // Scaling factor + const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + const scaledResult = (scaledCost * MTMFor1USDC) / scale; + return scaledResult; + })() + )} + // TODO: Use bn.js for better precision + mtmPrice={(() => { + const scale = BigInt(100); + const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + return (scaledCost * MTMFor1USDC) / scale; + })()} + /> ))} {/* Coming Soon Card */} diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index aaf48ca..c0910c5 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -5,10 +5,9 @@ import React, { useState } from 'react' interface AIServiceCardProps { title: string description: string - tokenCost: number isWalletConnected: boolean onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }> - mtmPrice: number | null + mtmPrice: bigint } interface GenerationState { @@ -21,7 +20,6 @@ interface GenerationState { const AIServiceCard: React.FC = ({ title, description, - tokenCost, isWalletConnected, onGenerate, mtmPrice @@ -83,7 +81,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM + Cost: {mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM
@@ -107,7 +105,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM & Generate`} diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index e7698d3..a947fb0 100644 --- a/src/services/fluxService.ts +++ b/src/services/fluxService.ts @@ -16,19 +16,19 @@ export const FLUX_MODELS: FluxModelConfig[] = [ modelId: "fal-ai/flux/schnell", name: "Schnell", description: "Fast meme generator", - cost: 0.1 + cost: 0.05 }, { modelId: "fal-ai/recraft-v3", name: "Recraft", description: "Advanced meme generator", - cost: 0.15 + cost: 0.10 }, { modelId: "fal-ai/stable-diffusion-v35-large", name: "Marquee", description: "Best meme generator", - cost: 0.2 + cost: 0.15 } ] diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 49956e1..dd12f4a 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -60,8 +60,10 @@ export async function getUSDCToMTMQuote() { } const quoteResponse = await response.json(); - const price = quoteResponse['data'][USDC_MINT]['price'] + // TODO: Use bn.js for better precision + const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price'])* (10 ** 6); + const price = BigInt(Number(priceFromAPI.toFixed(0))); return price; } @@ -71,7 +73,7 @@ interface WalletAdapter { export async function processMTMPayment( walletPublicKey: string, - tokenAmount: number, + tokenAmount: bigint, walletType: WalletType ): Promise { try { @@ -143,12 +145,14 @@ export async function processMTMPayment( ) } + const tokens = tokenAmount / BigInt(10 ** 6); + transaction.add( createTransferInstruction( senderATA, receiverATA, senderPublicKey, - BigInt(tokenAmount * (10 ** 6)) + tokens * BigInt(10 ** 6) ) ) @@ -157,7 +161,7 @@ export async function processMTMPayment( transaction.feePayer = senderPublicKey console.log('Sending transaction...') - const { signature } = await wallet.signAndSendTransaction(transaction) + const { signature } = await wallet.signAndSendTransaction(transaction); console.log('Transaction sent:', signature) const confirmation = await connection.confirmTransaction({ diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 770a984..0caf559 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -1,9 +1,13 @@ +import assert from 'assert'; + import { Connection } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { AppDataSource } from '../data-source'; import { Payment } from '../entity/Payment'; +assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); + const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL; @@ -42,7 +46,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise export async function verifyPayment( transactionSignature: string, - expectedAmount: number, + expectedAmount: bigint, ): Promise { try { // Check if the signature is already used @@ -67,7 +71,14 @@ export async function verifyPayment( const { info } = parsed; const { amount } = info; - if (BigInt(amount) === BigInt(expectedAmount * (10 ** 6))) { + const transactionAmount = BigInt(amount); + const expectedAmountInMicroTokens = expectedAmount; + + const lowerBound = expectedAmountInMicroTokens * BigInt(9) / BigInt(10); + const upperBound = expectedAmountInMicroTokens * BigInt(11) / BigInt(10); + + // transaction within the appropriate range + if (transactionAmount >= lowerBound && transactionAmount <= upperBound) { return true; } -- 2.45.2 From 53bd18f03147eb7ce45217756acd68552a5f96c7 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 18:34:03 +0530 Subject: [PATCH 04/20] Use bn.js to handle big numbers --- package-lock.json | 1 + package.json | 1 + src/app/api/flux/route.ts | 9 +++--- src/app/page.tsx | 51 +++++++++++++++----------------- src/components/AIServiceCard.tsx | 7 +++-- src/services/paymentService.ts | 15 +++++----- src/utils/verifyPayment.ts | 19 +++++++----- 7 files changed, 55 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index d46cd39..e12ba2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@google/generative-ai": "^0.21.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", + "bn.js": "^5.2.0", "next": "13.5.4", "openai": "^4.77.0", "react": "^18", diff --git a/package.json b/package.json index c1eeba9..d0b5887 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@google/generative-ai": "^0.21.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", + "bn.js": "^5.2.0", "next": "13.5.4", "openai": "^4.77.0", "react": "^18", diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index 4e1a52d..4651870 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -1,4 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' +import BN from 'bn.js'; + import { fal } from "@fal-ai/client" import { FLUX_MODELS } from '../../../services/fluxService' import { initializeDataSource } from '../../../data-source' @@ -49,11 +51,10 @@ export async function POST(req: NextRequest): Promise { const MTMFor1USDC = await getUSDCToMTMQuote(); - // TODO: Use bn.js for better precision - const scale = BigInt(100); - const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + const scale = new BN(100); + const scaledCost = new BN(model.cost * 100); - const expectedAmount = (scaledCost * MTMFor1USDC) / scale; + const expectedAmount = scaledCost.mul(MTMFor1USDC).div(scale); const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount) if (!isPaymentVerified) { diff --git a/src/app/page.tsx b/src/app/page.tsx index e462742..88e94c2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,8 @@ 'use client' import React, { useState, useEffect } from 'react' +import BN from 'bn.js'; + import WalletHeader from '../components/WalletHeader' import AIServiceCard from '../components/AIServiceCard' import { generateWithFlux, FluxGenerationResult, FLUX_MODELS } from '../services/fluxService' @@ -15,7 +17,7 @@ const Page: React.FC = (): React.ReactElement => { type: null }) - const [MTMFor1USDC, setMTMFor1USDC] = useState(BigInt(0)); + const [MTMFor1USDC, setMTMFor1USDC] = useState(new BN(0)); useEffect(() => { const fetchPrice = async () => { @@ -58,7 +60,7 @@ const Page: React.FC = (): React.ReactElement => { } }; - const handleFluxGeneration = (modelId: string, cost: bigint) => { + const handleFluxGeneration = (modelId: string, cost: BN) => { return async (prompt: string): Promise => { const { connected, publicKey, type } = walletState; @@ -111,36 +113,31 @@ const Page: React.FC = (): React.ReactElement => { walletState={walletState} onConnect={handleConnect} onDisconnect={handleDisconnect} - /> + /> {/* Flux Models Grid */}
- {FLUX_MODELS.map((model) => ( - { - const scale = BigInt(100); // Scaling factor - const scaledCost = BigInt(Math.round(model.cost * Number(scale))); - const scaledResult = (scaledCost * MTMFor1USDC) / scale; - return scaledResult; - })() - )} - // TODO: Use bn.js for better precision - mtmPrice={(() => { - const scale = BigInt(100); - const scaledCost = BigInt(Math.round(model.cost * Number(scale))); - return (scaledCost * MTMFor1USDC) / scale; - })()} - /> - ))} + {FLUX_MODELS.map((model) => { + // Convert cost from number to BN + const costBN = new BN(model.cost * 100); + const mtmPriceBN = costBN.mul(MTMFor1USDC); + return ( + + ); + })} {/* Coming Soon Card */}
diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index c0910c5..2745663 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -1,13 +1,14 @@ 'use client' import React, { useState } from 'react' +import BN from 'bn.js'; interface AIServiceCardProps { title: string description: string isWalletConnected: boolean onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }> - mtmPrice: bigint + mtmPrice: BN } interface GenerationState { @@ -81,7 +82,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM + Cost: {mtmPrice ? mtmPrice.div(new BN(10).pow(new BN(6))).toString() : '...'} MTM
@@ -105,7 +106,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? mtmPrice.div(new BN(10).pow(new BN(6))).toString() : '...'} MTM & Generate`}
diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index dd12f4a..b9eeb95 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import BN from 'bn.js'; import { Connection, PublicKey, Transaction } from '@solana/web3.js' import { @@ -50,7 +51,7 @@ async function findAssociatedTokenAddress( )[0] } -export async function getUSDCToMTMQuote() { +export async function getUSDCToMTMQuote(): Promise { const url = `https://api.jup.ag/price/v2?ids=${USDC_MINT}&vsToken=${MTM_TOKEN_MINT}`; const response = await fetch(url); @@ -61,9 +62,8 @@ export async function getUSDCToMTMQuote() { const quoteResponse = await response.json(); - // TODO: Use bn.js for better precision - const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price'])* (10 ** 6); - const price = BigInt(Number(priceFromAPI.toFixed(0))); + const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); + const price = new BN(priceFromAPI.toString().replace('.', '')); return price; } @@ -73,7 +73,7 @@ interface WalletAdapter { export async function processMTMPayment( walletPublicKey: string, - tokenAmount: bigint, + tokenAmount: BN, walletType: WalletType ): Promise { try { @@ -145,14 +145,15 @@ export async function processMTMPayment( ) } - const tokens = tokenAmount / BigInt(10 ** 6); + const tokens = tokenAmount.div(new BN(10).pow(new BN(6))); + console.log('TA:', tokenAmount.toString()) transaction.add( createTransferInstruction( senderATA, receiverATA, senderPublicKey, - tokens * BigInt(10 ** 6) + tokens.mul(new BN(10).pow(new BN(6))) ) ) diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 0caf559..19547d3 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import BN from 'bn.js'; import { Connection } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; @@ -46,7 +47,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise export async function verifyPayment( transactionSignature: string, - expectedAmount: bigint, + expectedAmount: BN, ): Promise { try { // Check if the signature is already used @@ -71,17 +72,21 @@ export async function verifyPayment( const { info } = parsed; const { amount } = info; - const transactionAmount = BigInt(amount); - const expectedAmountInMicroTokens = expectedAmount; + console.log('Parsed transfer instruction:', parsed); + console.log('Transfer info:', info); + console.log('Transfer amount:', amount); - const lowerBound = expectedAmountInMicroTokens * BigInt(9) / BigInt(10); - const upperBound = expectedAmountInMicroTokens * BigInt(11) / BigInt(10); + const transactionAmount = new BN(amount); + const lowerBound = expectedAmount.mul(new BN(9)).div(new BN(10)); + const upperBound = expectedAmount.mul(new BN(11)).div(new BN(10)); // transaction within the appropriate range - if (transactionAmount >= lowerBound && transactionAmount <= upperBound) { + if (transactionAmount.gte(lowerBound) && transactionAmount.lte(upperBound)) { + console.log('Transaction amount is within the expected range.'); return true; } - + console.log('Transaction amount is not within the expected range.'); + // TODO: Handle difference between paid amount and token price during verification being greater than 10% return false; } catch (error) { console.error('Verification error:', error); -- 2.45.2 From 5523939ff02928fde2f0d79e030517508ef55f4b Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 19:18:13 +0530 Subject: [PATCH 05/20] Display decimals in UI --- .env.example | 1 + package-lock.json | 11 +++++++++++ src/app/page.tsx | 6 +++--- src/components/AIServiceCard.tsx | 11 +++++++++-- src/services/paymentService.ts | 5 ++--- src/utils/verifyPayment.ts | 4 +++- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 9caa6f0..06f74b9 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ NEXT_PUBLIC_MTM_TOKEN_MINT=97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY NEXT_PUBLIC_SOLANA_RPC_URL=https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4 NEXT_PUBLIC_SOLANA_WEBSOCKET_URL=wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4 +NEXT_PUBLIC_USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e12ba2b..d9269ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "typeorm": "^0.3.12" }, "devDependencies": { + "@types/bn.js": "^5.1.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -791,6 +792,16 @@ "node": ">= 6" } }, + "node_modules/@types/bn.js": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.6.tgz", + "integrity": "sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", diff --git a/src/app/page.tsx b/src/app/page.tsx index 88e94c2..fe8278c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -121,7 +121,7 @@ const Page: React.FC = (): React.ReactElement => { {FLUX_MODELS.map((model) => { // Convert cost from number to BN const costBN = new BN(model.cost * 100); - const mtmPriceBN = costBN.mul(MTMFor1USDC); + const mtmPriceBN = costBN.mul(MTMFor1USDC).div(new BN(100)); return ( { onGenerate={handleFluxGeneration( model.modelId, // Calculate scaled cost directly in bn.js - mtmPriceBN.div(new BN(100)) + mtmPriceBN )} - mtmPrice={mtmPriceBN.div(new BN(100))} + mtmPrice={mtmPriceBN} /> ); })} diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 2745663..68c2a2e 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -18,6 +18,13 @@ interface GenerationState { error: string | null } +const formatBNWithDecimals = (value: BN, decimals: number): string => { + const quotient = value.div(new BN(10).pow(new BN(decimals))); + const remainder = value.mod(new BN(10).pow(new BN(decimals))); + const decimalPart = remainder.mul(new BN(10).pow(new BN(6))).div(new BN(10).pow(new BN(decimals))); + return `${quotient.toString()}.${decimalPart.toString().padStart(6, '0')}`; +} + const AIServiceCard: React.FC = ({ title, description, @@ -82,7 +89,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {mtmPrice ? mtmPrice.div(new BN(10).pow(new BN(6))).toString() : '...'} MTM + Cost: {mtmPrice ? formatBNWithDecimals(mtmPrice, 6) : '...'} MTM
@@ -106,7 +113,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? mtmPrice.div(new BN(10).pow(new BN(6))).toString() : '...'} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? formatBNWithDecimals(mtmPrice, 6) : '...'} MTM & Generate`} diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index b9eeb95..6660895 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -145,15 +145,14 @@ export async function processMTMPayment( ) } - const tokens = tokenAmount.div(new BN(10).pow(new BN(6))); + const amountToSend = BigInt(tokenAmount.toString()); - console.log('TA:', tokenAmount.toString()) transaction.add( createTransferInstruction( senderATA, receiverATA, senderPublicKey, - tokens.mul(new BN(10).pow(new BN(6))) + amountToSend ) ) diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 19547d3..c2dd89a 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -77,6 +77,8 @@ export async function verifyPayment( console.log('Transfer amount:', amount); const transactionAmount = new BN(amount); + + // Transaction amount should be within 10% of the expected amount const lowerBound = expectedAmount.mul(new BN(9)).div(new BN(10)); const upperBound = expectedAmount.mul(new BN(11)).div(new BN(10)); @@ -86,7 +88,7 @@ export async function verifyPayment( return true; } console.log('Transaction amount is not within the expected range.'); - // TODO: Handle difference between paid amount and token price during verification being greater than 10% + // TODO: Handle spillage being greater than 10% return false; } catch (error) { console.error('Verification error:', error); -- 2.45.2 From 5fd10ae15f8c73c7b4e46334ea3ba368bf6c6c3a Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 19:21:17 +0530 Subject: [PATCH 06/20] Update comment --- src/app/page.tsx | 4 ++-- src/services/paymentService.ts | 2 +- src/utils/verifyPayment.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index fe8278c..65ae898 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -56,9 +56,9 @@ const Page: React.FC = (): React.ReactElement => { connected: false, publicKey: null, type: null - }); + }) } - }; + } const handleFluxGeneration = (modelId: string, cost: BN) => { return async (prompt: string): Promise => { diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 6660895..501e2b8 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -161,7 +161,7 @@ export async function processMTMPayment( transaction.feePayer = senderPublicKey console.log('Sending transaction...') - const { signature } = await wallet.signAndSendTransaction(transaction); + const { signature } = await wallet.signAndSendTransaction(transaction) console.log('Transaction sent:', signature) const confirmation = await connection.confirmTransaction({ diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index c2dd89a..2998edd 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -88,7 +88,7 @@ export async function verifyPayment( return true; } console.log('Transaction amount is not within the expected range.'); - // TODO: Handle spillage being greater than 10% + // TODO: Handle slippage being greater than 10% return false; } catch (error) { console.error('Verification error:', error); -- 2.45.2 From 771656ae5dbad136c3b0141cf9b9da6d2e39df16 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Tue, 28 Jan 2025 19:56:41 +0530 Subject: [PATCH 07/20] Simplify function to display price with decimal point --- package.json | 1 + src/app/api/flux/route.ts | 4 ++-- src/app/page.tsx | 6 +++--- src/components/AIServiceCard.tsx | 10 ++++++---- src/utils/verifyPayment.ts | 4 ---- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index d0b5887..713bb26 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "typeorm": "^0.3.12" }, "devDependencies": { + "@types/bn.js": "^5.1.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index 4651870..f3e0734 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -49,12 +49,12 @@ export async function POST(req: NextRequest): Promise { ) } - const MTMFor1USDC = await getUSDCToMTMQuote(); + const mtmFor1USDC = await getUSDCToMTMQuote(); const scale = new BN(100); const scaledCost = new BN(model.cost * 100); - const expectedAmount = scaledCost.mul(MTMFor1USDC).div(scale); + const expectedAmount = scaledCost.mul(mtmFor1USDC).div(scale); const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount) if (!isPaymentVerified) { diff --git a/src/app/page.tsx b/src/app/page.tsx index 65ae898..70cf77d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -17,13 +17,13 @@ const Page: React.FC = (): React.ReactElement => { type: null }) - const [MTMFor1USDC, setMTMFor1USDC] = useState(new BN(0)); + const [mtmFor1USDC, setMtmFor1USDC] = useState(new BN(0)); useEffect(() => { const fetchPrice = async () => { try { const price = await getUSDCToMTMQuote(); - setMTMFor1USDC(price); + setMtmFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); } @@ -121,7 +121,7 @@ const Page: React.FC = (): React.ReactElement => { {FLUX_MODELS.map((model) => { // Convert cost from number to BN const costBN = new BN(model.cost * 100); - const mtmPriceBN = costBN.mul(MTMFor1USDC).div(new BN(100)); + const mtmPriceBN = costBN.mul(mtmFor1USDC).div(new BN(100)); return ( { - const quotient = value.div(new BN(10).pow(new BN(decimals))); - const remainder = value.mod(new BN(10).pow(new BN(decimals))); - const decimalPart = remainder.mul(new BN(10).pow(new BN(6))).div(new BN(10).pow(new BN(decimals))); - return `${quotient.toString()}.${decimalPart.toString().padStart(6, '0')}`; + const factor = new BN(10).pow(new BN(decimals)); + // Part before decimal point + const quotient = value.div(factor); + // Part after decimal point + const remainder = value.mod(factor); + return `${quotient.toString()}.${remainder.toString().padStart(decimals, '0')}`; } const AIServiceCard: React.FC = ({ diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 2998edd..b47e39a 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -72,10 +72,6 @@ export async function verifyPayment( const { info } = parsed; const { amount } = info; - console.log('Parsed transfer instruction:', parsed); - console.log('Transfer info:', info); - console.log('Transfer amount:', amount); - const transactionAmount = new BN(amount); // Transaction amount should be within 10% of the expected amount -- 2.45.2 From 0944a9de685da7871e36db5537d23242c780f49d Mon Sep 17 00:00:00 2001 From: Nabarun Date: Tue, 28 Jan 2025 21:45:45 +0530 Subject: [PATCH 08/20] Use USDC mint from env --- src/services/paymentService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 501e2b8..1adfc9c 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -14,13 +14,13 @@ import { WalletType } from './types' assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); assert(process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS, 'PAYMENT_RECEIVER_ADDRESS is required'); +assert(process.env.NEXT_PUBLIC_USDC_MINT, 'USDC_MINT is required'); const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL; - -const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC mint address +const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; const connection = new Connection( SOLANA_RPC_URL, -- 2.45.2 From 8ed2c29c445677658ba7c65a8cc2b36fb134a893 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Wed, 29 Jan 2025 19:09:48 +0530 Subject: [PATCH 09/20] Add custom server for storing quotes in cache --- .gitignore | 1 + cache.ts | 37 ++++++++++ package-lock.json | 165 ++++++++++++++++++++++++++++++++++++++++++- package.json | 8 ++- server.ts | 31 ++++++++ src/app/page.tsx | 10 ++- tsconfig.server.json | 12 ++++ 7 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 cache.ts create mode 100644 server.ts create mode 100644 tsconfig.server.json diff --git a/.gitignore b/.gitignore index 6768f48..88281b9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules .env.test database.sqlite +dist diff --git a/cache.ts b/cache.ts new file mode 100644 index 0000000..aa2eb8e --- /dev/null +++ b/cache.ts @@ -0,0 +1,37 @@ +import assert from "assert"; +import BN from "bn.js"; +import fetch from 'node-fetch'; + +let cachedQuotes: BN[] = []; + +assert(process.env.NEXT_PUBLIC_USDC_MINT, 'USDC_MINT is required'); +assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); + +const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; +const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; + +export async function fetchAndCacheQuotes(): Promise { + try { + const url = `https://api.jup.ag/price/v2?ids=${USDC_MINT}&vsToken=${MTM_TOKEN_MINT}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch quote: ${response.statusText}`); + } + + const quoteResponse = await response.json(); + const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); + const price = new BN(priceFromAPI.toString().replace('.', '')); + + cachedQuotes.push(price); + if (cachedQuotes.length > 3) { + cachedQuotes.shift(); + } + } catch (error) { + console.error('Error fetching quotes:', error); + } +} + +export function getLatestQuote(): BN | undefined { + return cachedQuotes[cachedQuotes.length - 1]; +} diff --git a/package-lock.json b/package-lock.json index d9269ca..e286f6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", "bn.js": "^5.2.0", + "dotenv": "^16.4.7", "next": "13.5.4", "openai": "^4.77.0", "react": "^18", @@ -30,6 +31,7 @@ "eslint-config-next": "13.5.4", "postcss": "^8", "tailwindcss": "^3", + "ts-node": "^10.9.2", "typescript": "^5" } }, @@ -56,6 +58,30 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -281,7 +307,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -299,7 +325,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -792,6 +818,34 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/bn.js": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.6.tgz", @@ -1000,7 +1054,7 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -1017,6 +1071,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2063,6 +2130,13 @@ "license": "ISC", "optional": true }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2278,6 +2352,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4658,6 +4742,13 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", @@ -7085,6 +7176,57 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -7517,6 +7659,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -7876,6 +8025,16 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 713bb26..ba3c441 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", + "dev": "ts-node --project tsconfig.server.json server.ts", + "build": "next build && tsc --project tsconfig.server.json", + "start": "NODE_ENV=production node dist/server.js", "lint": "next lint" }, "dependencies": { @@ -14,6 +14,7 @@ "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", "bn.js": "^5.2.0", + "dotenv": "^16.4.7", "next": "13.5.4", "openai": "^4.77.0", "react": "^18", @@ -31,6 +32,7 @@ "eslint-config-next": "13.5.4", "postcss": "^8", "tailwindcss": "^3", + "ts-node": "^10.9.2", "typescript": "^5" } } diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..fceaefe --- /dev/null +++ b/server.ts @@ -0,0 +1,31 @@ +import { createServer } from 'http'; +import { parse } from 'url'; +import next from 'next'; +import dotenv from 'dotenv'; +dotenv.config(); + +import { fetchAndCacheQuotes, getLatestQuote } from './cache'; + +const port = parseInt(process.env.PORT || '3000', 10); +const app = next({ dev: process.env.NODE_ENV !== 'production' }); +const handle = app.getRequestHandler(); + +app.prepare().then(() => { + const server = createServer(async (req, res) => { + const parsedUrl = parse(req.url!, true); + (req as any).getLatestQuote = getLatestQuote; + + // Default Next.js request handling + handle(req, res, parsedUrl); + }); + + server.listen(port, () => { + console.log(`> Server listening at http://localhost:${port}`); + }); + + // Initial call and interval setup + fetchAndCacheQuotes(); // Initial store + setInterval(fetchAndCacheQuotes, 5 * 60 * 1000); // Update cache every 5 minutes +}); + +export { getLatestQuote }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 70cf77d..e78ea38 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,7 +6,7 @@ import BN from 'bn.js'; import WalletHeader from '../components/WalletHeader' import AIServiceCard from '../components/AIServiceCard' import { generateWithFlux, FluxGenerationResult, FLUX_MODELS } from '../services/fluxService' -import { getUSDCToMTMQuote, processMTMPayment } from '../services/paymentService' +import { processMTMPayment } from '../services/paymentService' import { connectWallet, disconnectWallet, WalletState } from '../services/walletService' import { WalletType } from '../services/types' @@ -22,17 +22,15 @@ const Page: React.FC = (): React.ReactElement => { useEffect(() => { const fetchPrice = async () => { try { - const price = await getUSDCToMTMQuote(); - setMtmFor1USDC(price); + const response = await fetch('/api/quotes'); + const data = await response.json(); + setMtmFor1USDC(new BN(data.latestQuote)); // Convert the string back to BN } catch (error) { console.error('Failed to fetch price:', error); } }; fetchPrice(); - const interval = setInterval(fetchPrice, 10000); - - return () => clearInterval(interval); }, []); const handleConnect = async (walletType: WalletType): Promise => { diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..6a8dc0a --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "dist", + "lib": ["es2019"], + "target": "es2019", + "isolatedModules": false, + "noEmit": false + }, + "include": ["server.ts"] +} \ No newline at end of file -- 2.45.2 From c1b0ad6ec541bf0c51bbac2743604969959ef56f Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 10:16:05 +0530 Subject: [PATCH 10/20] Add API for getting latest quote --- server.ts | 18 ++++++++++++++++-- src/app/api/quotes/route.ts | 6 ++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/app/api/quotes/route.ts diff --git a/server.ts b/server.ts index fceaefe..16d841e 100644 --- a/server.ts +++ b/server.ts @@ -10,6 +10,22 @@ const port = parseInt(process.env.PORT || '3000', 10); const app = next({ dev: process.env.NODE_ENV !== 'production' }); const handle = app.getRequestHandler(); +declare global { + namespace NodeJS { + interface Global { + quoteService: { + getLatestQuote: typeof getLatestQuote; + } + } + } +} + +// TODO: Look for a better way to use quoteService +// Initialize the global quote service +(global as any).quoteService = { + getLatestQuote +}; + app.prepare().then(() => { const server = createServer(async (req, res) => { const parsedUrl = parse(req.url!, true); @@ -27,5 +43,3 @@ app.prepare().then(() => { fetchAndCacheQuotes(); // Initial store setInterval(fetchAndCacheQuotes, 5 * 60 * 1000); // Update cache every 5 minutes }); - -export { getLatestQuote }; diff --git a/src/app/api/quotes/route.ts b/src/app/api/quotes/route.ts new file mode 100644 index 0000000..ee8f78d --- /dev/null +++ b/src/app/api/quotes/route.ts @@ -0,0 +1,6 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + const quote = (global as any).quoteService.getLatestQuote(); + return NextResponse.json(quote); +} -- 2.45.2 From f712adfec207f0a3a8c221aa35e73edbfe1efd67 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 10:43:52 +0530 Subject: [PATCH 11/20] Create class for quotes service --- cache.ts | 37 ------------------------------------- quotes-service.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ server.ts | 22 ++++++++++------------ 3 files changed, 53 insertions(+), 49 deletions(-) delete mode 100644 cache.ts create mode 100644 quotes-service.ts diff --git a/cache.ts b/cache.ts deleted file mode 100644 index aa2eb8e..0000000 --- a/cache.ts +++ /dev/null @@ -1,37 +0,0 @@ -import assert from "assert"; -import BN from "bn.js"; -import fetch from 'node-fetch'; - -let cachedQuotes: BN[] = []; - -assert(process.env.NEXT_PUBLIC_USDC_MINT, 'USDC_MINT is required'); -assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); - -const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; -const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; - -export async function fetchAndCacheQuotes(): Promise { - try { - const url = `https://api.jup.ag/price/v2?ids=${USDC_MINT}&vsToken=${MTM_TOKEN_MINT}`; - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Failed to fetch quote: ${response.statusText}`); - } - - const quoteResponse = await response.json(); - const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); - const price = new BN(priceFromAPI.toString().replace('.', '')); - - cachedQuotes.push(price); - if (cachedQuotes.length > 3) { - cachedQuotes.shift(); - } - } catch (error) { - console.error('Error fetching quotes:', error); - } -} - -export function getLatestQuote(): BN | undefined { - return cachedQuotes[cachedQuotes.length - 1]; -} diff --git a/quotes-service.ts b/quotes-service.ts new file mode 100644 index 0000000..1904eca --- /dev/null +++ b/quotes-service.ts @@ -0,0 +1,43 @@ +import assert from "assert"; +import BN from "bn.js"; +import fetch from 'node-fetch'; + +assert(process.env.NEXT_PUBLIC_USDC_MINT, 'USDC_MINT is required'); +assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); + +const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; +const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; + +class QuotesService { + private cachedQuotes: BN[] = []; + + async fetchAndCacheQuotes(): Promise { + try { + const url = `https://api.jup.ag/price/v2?ids=${USDC_MINT}&vsToken=${MTM_TOKEN_MINT}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch quote: ${response.statusText}`); + } + + const quoteResponse = await response.json(); + const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); + const price = new BN(priceFromAPI.toString().replace('.', '')); + + this.cachedQuotes.push(price); + if (this.cachedQuotes.length > 3) { + this.cachedQuotes.shift(); + } + console.log('Cache updated: ', this.cachedQuotes); + } catch (error) { + console.error('Error fetching quotes:', error); + } + } + + getLatestQuote(): BN | undefined { + console.log({ cachedQuotes: this.cachedQuotes }); + return this.cachedQuotes[this.cachedQuotes.length - 1]; + } +} + +export { QuotesService }; diff --git a/server.ts b/server.ts index 16d841e..4d6c2a0 100644 --- a/server.ts +++ b/server.ts @@ -4,32 +4,30 @@ import next from 'next'; import dotenv from 'dotenv'; dotenv.config(); -import { fetchAndCacheQuotes, getLatestQuote } from './cache'; +import { QuotesService } from './quotes-service'; const port = parseInt(process.env.PORT || '3000', 10); const app = next({ dev: process.env.NODE_ENV !== 'production' }); const handle = app.getRequestHandler(); +const quotesService = new QuotesService(); + declare global { namespace NodeJS { interface Global { - quoteService: { - getLatestQuote: typeof getLatestQuote; - } - } + quotesService: typeof quotesService } } +} -// TODO: Look for a better way to use quoteService +// TODO: Look for a better way to use quotesService // Initialize the global quote service -(global as any).quoteService = { - getLatestQuote -}; +(global as any).quotesService = quotesService app.prepare().then(() => { const server = createServer(async (req, res) => { const parsedUrl = parse(req.url!, true); - (req as any).getLatestQuote = getLatestQuote; + (req as any).quotesService = quotesService; // Default Next.js request handling handle(req, res, parsedUrl); @@ -40,6 +38,6 @@ app.prepare().then(() => { }); // Initial call and interval setup - fetchAndCacheQuotes(); // Initial store - setInterval(fetchAndCacheQuotes, 5 * 60 * 1000); // Update cache every 5 minutes + quotesService.fetchAndCacheQuotes(); // Initial store + setInterval(() => quotesService.fetchAndCacheQuotes(), 5 * 60 * 1000); // Update cache every 5 minutes }); -- 2.45.2 From ddda589da95cc40bf7763e034b0e647ad40a6926 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 11:47:28 +0530 Subject: [PATCH 12/20] Use backend cache for getting quotes during payment verification --- quotes-service.ts | 5 ++--- src/app/api/flux/route.ts | 13 ++++++------- src/app/api/quotes/route.ts | 10 ++++++++-- src/app/page.tsx | 5 ++++- src/services/fluxService.ts | 2 ++ src/services/paymentService.ts | 18 ------------------ src/utils/verifyPayment.ts | 10 ++-------- 7 files changed, 24 insertions(+), 39 deletions(-) diff --git a/quotes-service.ts b/quotes-service.ts index 1904eca..9ff7481 100644 --- a/quotes-service.ts +++ b/quotes-service.ts @@ -34,9 +34,8 @@ class QuotesService { } } - getLatestQuote(): BN | undefined { - console.log({ cachedQuotes: this.cachedQuotes }); - return this.cachedQuotes[this.cachedQuotes.length - 1]; + getQuotes(): BN[] { + return this.cachedQuotes; } } diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index f3e0734..2da9c52 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -4,9 +4,7 @@ import BN from 'bn.js'; import { fal } from "@fal-ai/client" import { FLUX_MODELS } from '../../../services/fluxService' import { initializeDataSource } from '../../../data-source' - -import { verifyPayment, markSignatureAsUsed } from '../../../utils/verifyPayment' -import { getUSDCToMTMQuote } from '../../../services/paymentService' +import { verifyPayment, markSignatureAsUsed } from '../../../utils/verifyPayment'; if (!process.env.FAL_AI_KEY) { throw new Error('FAL_AI_KEY is not configured in environment variables') @@ -49,17 +47,18 @@ export async function POST(req: NextRequest): Promise { ) } - const mtmFor1USDC = await getUSDCToMTMQuote(); + const quotes: BN[] = (global as any).quotesService.getQuotes(); + const mtmFor1USDC = quotes.reduce((min, quote) => quote.lt(min) ? quote : min, quotes[0]); // Take the least cost in the array const scale = new BN(100); const scaledCost = new BN(model.cost * 100); - const expectedAmount = scaledCost.mul(mtmFor1USDC).div(scale); - const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount) + const lowestQuote = scaledCost.mul(new BN(mtmFor1USDC)).div(scale); + const isPaymentVerified = await verifyPayment(transactionSignature, lowestQuote); if (!isPaymentVerified) { return NextResponse.json( - { error: 'Payment verification failed or transaction signature has already been used' }, + { error: 'Payment verification failed or transaction signature has already been used', reload: true }, { status: 400 } ) } diff --git a/src/app/api/quotes/route.ts b/src/app/api/quotes/route.ts index ee8f78d..8ef3744 100644 --- a/src/app/api/quotes/route.ts +++ b/src/app/api/quotes/route.ts @@ -1,6 +1,12 @@ +import BN from 'bn.js'; import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { - const quote = (global as any).quoteService.getLatestQuote(); - return NextResponse.json(quote); + try { + const quotes: BN[] = (global as any).quotesService.getQuotes(); + const quotesAsString = quotes.map(quote => quote.toString()); + return NextResponse.json(quotesAsString); + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch quotes' }, { status: 500 }); + } } diff --git a/src/app/page.tsx b/src/app/page.tsx index e78ea38..e01cba9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -24,7 +24,10 @@ const Page: React.FC = (): React.ReactElement => { try { const response = await fetch('/api/quotes'); const data = await response.json(); - setMtmFor1USDC(new BN(data.latestQuote)); // Convert the string back to BN + + // Convert the string back to BN + const price = new BN(data[data.length - 1]); + setMtmFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); } diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index a947fb0..77ec20c 100644 --- a/src/services/fluxService.ts +++ b/src/services/fluxService.ts @@ -65,6 +65,8 @@ export async function generateWithFlux( } } catch (error) { console.error('Flux generation error:', error) + // Reload the page to get latest prices + window.location.reload(); return { error: error instanceof Error ? error.message : 'Generation failed' } diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 1adfc9c..6e809a2 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -14,13 +14,11 @@ import { WalletType } from './types' assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); assert(process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS, 'PAYMENT_RECEIVER_ADDRESS is required'); -assert(process.env.NEXT_PUBLIC_USDC_MINT, 'USDC_MINT is required'); const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL; -const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; const connection = new Connection( SOLANA_RPC_URL, @@ -51,22 +49,6 @@ async function findAssociatedTokenAddress( )[0] } -export async function getUSDCToMTMQuote(): Promise { - const url = `https://api.jup.ag/price/v2?ids=${USDC_MINT}&vsToken=${MTM_TOKEN_MINT}`; - - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Failed to fetch quote: ${response.statusText}`); - } - - const quoteResponse = await response.json(); - - const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); - const price = new BN(priceFromAPI.toString().replace('.', '')); - return price; -} - interface WalletAdapter { signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }> } diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index b47e39a..c3427a5 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -47,7 +47,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise export async function verifyPayment( transactionSignature: string, - expectedAmount: BN, + lowestQuote: BN, ): Promise { try { // Check if the signature is already used @@ -74,17 +74,11 @@ export async function verifyPayment( const transactionAmount = new BN(amount); - // Transaction amount should be within 10% of the expected amount - const lowerBound = expectedAmount.mul(new BN(9)).div(new BN(10)); - const upperBound = expectedAmount.mul(new BN(11)).div(new BN(10)); - - // transaction within the appropriate range - if (transactionAmount.gte(lowerBound) && transactionAmount.lte(upperBound)) { + if (transactionAmount.gte(lowestQuote)) { console.log('Transaction amount is within the expected range.'); return true; } console.log('Transaction amount is not within the expected range.'); - // TODO: Handle slippage being greater than 10% return false; } catch (error) { console.error('Verification error:', error); -- 2.45.2 From de0b4d42c2a59a5c9444f6dcc4c0b0e2cb3725d9 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 12:07:43 +0530 Subject: [PATCH 13/20] Update variable --- server.ts | 3 +-- src/app/api/flux/route.ts | 6 +++--- src/app/page.tsx | 10 +++++----- src/components/AIServiceCard.tsx | 8 ++++---- src/utils/verifyPayment.ts | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/server.ts b/server.ts index 4d6c2a0..dcba014 100644 --- a/server.ts +++ b/server.ts @@ -21,7 +21,7 @@ declare global { } // TODO: Look for a better way to use quotesService -// Initialize the global quote service +// Initialize global quotes service (global as any).quotesService = quotesService app.prepare().then(() => { @@ -29,7 +29,6 @@ app.prepare().then(() => { const parsedUrl = parse(req.url!, true); (req as any).quotesService = quotesService; - // Default Next.js request handling handle(req, res, parsedUrl); }); diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index 2da9c52..0d90b55 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -48,17 +48,17 @@ export async function POST(req: NextRequest): Promise { } const quotes: BN[] = (global as any).quotesService.getQuotes(); - const mtmFor1USDC = quotes.reduce((min, quote) => quote.lt(min) ? quote : min, quotes[0]); // Take the least cost in the array + const priceMTMFor1USDC = quotes.reduce((min, quote) => quote.lt(min) ? quote : min, quotes[0]); // Take the minimum quote from the array const scale = new BN(100); const scaledCost = new BN(model.cost * 100); - const lowestQuote = scaledCost.mul(new BN(mtmFor1USDC)).div(scale); + const lowestQuote = scaledCost.mul(new BN(priceMTMFor1USDC)).div(scale); const isPaymentVerified = await verifyPayment(transactionSignature, lowestQuote); if (!isPaymentVerified) { return NextResponse.json( - { error: 'Payment verification failed or transaction signature has already been used', reload: true }, + { error: 'Payment verification failed or transaction signature has already been used' }, { status: 400 } ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index e01cba9..0a1461c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -17,7 +17,7 @@ const Page: React.FC = (): React.ReactElement => { type: null }) - const [mtmFor1USDC, setMtmFor1USDC] = useState(new BN(0)); + const [priceMTMFor1USDC, setPriceMTMFor1USDC] = useState(new BN(0)); useEffect(() => { const fetchPrice = async () => { @@ -27,7 +27,7 @@ const Page: React.FC = (): React.ReactElement => { // Convert the string back to BN const price = new BN(data[data.length - 1]); - setMtmFor1USDC(price); + setPriceMTMFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); } @@ -122,7 +122,7 @@ const Page: React.FC = (): React.ReactElement => { {FLUX_MODELS.map((model) => { // Convert cost from number to BN const costBN = new BN(model.cost * 100); - const mtmPriceBN = costBN.mul(mtmFor1USDC).div(new BN(100)); + const priceMTM = costBN.mul(priceMTMFor1USDC).div(new BN(100)); return ( { onGenerate={handleFluxGeneration( model.modelId, // Calculate scaled cost directly in bn.js - mtmPriceBN + priceMTM )} - mtmPrice={mtmPriceBN} + priceMTM={priceMTM} /> ); })} diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 27a3f59..2f73b79 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -8,7 +8,7 @@ interface AIServiceCardProps { description: string isWalletConnected: boolean onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }> - mtmPrice: BN + priceMTM: BN } interface GenerationState { @@ -32,7 +32,7 @@ const AIServiceCard: React.FC = ({ description, isWalletConnected, onGenerate, - mtmPrice + priceMTM }) => { const [inputText, setInputText] = useState('') const [generationState, setGenerationState] = useState({ @@ -91,7 +91,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {mtmPrice ? formatBNWithDecimals(mtmPrice, 6) : '...'} MTM + Cost: {priceMTM ? formatBNWithDecimals(priceMTM, 6) : '...'} MTM
@@ -115,7 +115,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? formatBNWithDecimals(mtmPrice, 6) : '...'} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${priceMTM ? formatBNWithDecimals(priceMTM, 6) : '...'} MTM & Generate`} diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index c3427a5..145ab21 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -75,10 +75,10 @@ export async function verifyPayment( const transactionAmount = new BN(amount); if (transactionAmount.gte(lowestQuote)) { - console.log('Transaction amount is within the expected range.'); + console.log('Transaction amount is greater than minimum amount'); return true; } - console.log('Transaction amount is not within the expected range.'); + console.log('Transaction amount is less than minimum amount. Rejecting request'); return false; } catch (error) { console.error('Verification error:', error); -- 2.45.2 From cc64e6a035ff6204319537d77bb67e64f3610a3b Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 12:30:36 +0530 Subject: [PATCH 14/20] Display message in UI before reloading --- src/components/AIServiceCard.tsx | 7 +++++-- src/services/fluxService.ts | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 2f73b79..52ddfef 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -60,7 +60,10 @@ const AIServiceCard: React.FC = ({ loading: false, error: result.error, }) - return + // Reload the page to get latest prices + setTimeout(() => { + window.location.reload(); + }, 3000); } if (result.imageUrl) { @@ -121,7 +124,7 @@ const AIServiceCard: React.FC = ({ {generationState.error && (
- {generationState.error} + {generationState.error}, reloading...
)} diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index 77ec20c..a947fb0 100644 --- a/src/services/fluxService.ts +++ b/src/services/fluxService.ts @@ -65,8 +65,6 @@ export async function generateWithFlux( } } catch (error) { console.error('Flux generation error:', error) - // Reload the page to get latest prices - window.location.reload(); return { error: error instanceof Error ? error.message : 'Generation failed' } -- 2.45.2 From 2e735c51dd0f42239d61c41194a2f16b9a10f8fb Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 14:41:29 +0530 Subject: [PATCH 15/20] Use big.js for handling decimals --- package-lock.json | 14 ++++++++++++++ package.json | 1 + quotes-service.ts | 8 +++++--- server.ts | 4 +++- src/app/api/flux/route.ts | 6 +++--- src/app/page.tsx | 1 - src/components/AIServiceCard.tsx | 17 ++++++++++------- src/services/fluxService.ts | 2 +- src/utils/verifyPayment.ts | 5 ++--- 9 files changed, 39 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index e286f6a..127b13e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@google/generative-ai": "^0.21.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", + "big.js": "^6.2.2", "bn.js": "^5.2.0", "dotenv": "^16.4.7", "next": "13.5.4", @@ -1501,6 +1502,19 @@ } ] }, + "node_modules/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, "node_modules/bigint-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", diff --git a/package.json b/package.json index ba3c441..48b3908 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@google/generative-ai": "^0.21.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", + "big.js": "^6.2.2", "bn.js": "^5.2.0", "dotenv": "^16.4.7", "next": "13.5.4", diff --git a/quotes-service.ts b/quotes-service.ts index 9ff7481..bfdb871 100644 --- a/quotes-service.ts +++ b/quotes-service.ts @@ -1,6 +1,7 @@ import assert from "assert"; import BN from "bn.js"; import fetch from 'node-fetch'; +import Big from 'big.js'; assert(process.env.NEXT_PUBLIC_USDC_MINT, 'USDC_MINT is required'); assert(process.env.NEXT_PUBLIC_MTM_TOKEN_MINT, 'MTM_TOKEN_MINT is required'); @@ -21,14 +22,15 @@ class QuotesService { } const quoteResponse = await response.json(); - const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); - const price = new BN(priceFromAPI.toString().replace('.', '')); + + // Handle price with Big.js, then convert to BN + const priceFromAPI = new Big(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); + const price = new BN(new Big(priceFromAPI).times(new Big(10).pow(6)).toString()); this.cachedQuotes.push(price); if (this.cachedQuotes.length > 3) { this.cachedQuotes.shift(); } - console.log('Cache updated: ', this.cachedQuotes); } catch (error) { console.error('Error fetching quotes:', error); } diff --git a/server.ts b/server.ts index dcba014..8ee34d4 100644 --- a/server.ts +++ b/server.ts @@ -1,6 +1,8 @@ import { createServer } from 'http'; import { parse } from 'url'; import next from 'next'; + +// Reference: https://www.dotenv.org/docs/quickstart#initial-setup import dotenv from 'dotenv'; dotenv.config(); @@ -16,9 +18,9 @@ declare global { namespace NodeJS { interface Global { quotesService: typeof quotesService + } } } -} // TODO: Look for a better way to use quotesService // Initialize global quotes service diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index 0d90b55..8b7601d 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -48,13 +48,13 @@ export async function POST(req: NextRequest): Promise { } const quotes: BN[] = (global as any).quotesService.getQuotes(); - const priceMTMFor1USDC = quotes.reduce((min, quote) => quote.lt(min) ? quote : min, quotes[0]); // Take the minimum quote from the array + const lowestQuote = quotes.reduce((minQuote, currentQuote) => BN.min(minQuote, currentQuote), quotes[0]); const scale = new BN(100); const scaledCost = new BN(model.cost * 100); - const lowestQuote = scaledCost.mul(new BN(priceMTMFor1USDC)).div(scale); - const isPaymentVerified = await verifyPayment(transactionSignature, lowestQuote); + const tokenAmount = scaledCost.mul(new BN(lowestQuote)).div(scale); + const isPaymentVerified = await verifyPayment(transactionSignature, tokenAmount); if (!isPaymentVerified) { return NextResponse.json( diff --git a/src/app/page.tsx b/src/app/page.tsx index 0a1461c..a147ef1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -132,7 +132,6 @@ const Page: React.FC = (): React.ReactElement => { isWalletConnected={walletState.connected} onGenerate={handleFluxGeneration( model.modelId, - // Calculate scaled cost directly in bn.js priceMTM )} priceMTM={priceMTM} diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 52ddfef..547b8bd 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react' import BN from 'bn.js'; +import Big from 'big.js'; interface AIServiceCardProps { title: string @@ -19,12 +20,12 @@ interface GenerationState { } const formatBNWithDecimals = (value: BN, decimals: number): string => { - const factor = new BN(10).pow(new BN(decimals)); - // Part before decimal point - const quotient = value.div(factor); - // Part after decimal point - const remainder = value.mod(factor); - return `${quotient.toString()}.${remainder.toString().padStart(decimals, '0')}`; + if (value.isZero()) return '0.' + '0'.repeat(decimals); // Handle zero case + + const bigValue = new Big(value.toString()); + const factor = new Big(10).pow(decimals); + + return bigValue.div(factor).toFixed(decimals); } const AIServiceCard: React.FC = ({ @@ -64,6 +65,8 @@ const AIServiceCard: React.FC = ({ setTimeout(() => { window.location.reload(); }, 3000); + + return } if (result.imageUrl) { @@ -124,7 +127,7 @@ const AIServiceCard: React.FC = ({ {generationState.error && (
- {generationState.error}, reloading... + {generationState.error}
)} diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index a947fb0..14c68a0 100644 --- a/src/services/fluxService.ts +++ b/src/services/fluxService.ts @@ -51,7 +51,7 @@ export async function generateWithFlux( }) if (!response.ok) { - throw new Error('Failed to generate image') + throw new Error('Failed to generate image, reloading...') } const data = await response.json() diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 145ab21..2b5f91a 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -47,7 +47,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise export async function verifyPayment( transactionSignature: string, - lowestQuote: BN, + tokenAmount: BN, ): Promise { try { // Check if the signature is already used @@ -74,8 +74,7 @@ export async function verifyPayment( const transactionAmount = new BN(amount); - if (transactionAmount.gte(lowestQuote)) { - console.log('Transaction amount is greater than minimum amount'); + if (transactionAmount.gte(tokenAmount)) { return true; } console.log('Transaction amount is less than minimum amount. Rejecting request'); -- 2.45.2 From b886aca06703a295688ab0c26457302547699cf4 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 15:45:17 +0530 Subject: [PATCH 16/20] Remove quotes service injection in server --- quotes-service.ts | 6 +++--- server.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/quotes-service.ts b/quotes-service.ts index bfdb871..bb3d72a 100644 --- a/quotes-service.ts +++ b/quotes-service.ts @@ -24,10 +24,10 @@ class QuotesService { const quoteResponse = await response.json(); // Handle price with Big.js, then convert to BN - const priceFromAPI = new Big(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); - const price = new BN(new Big(priceFromAPI).times(new Big(10).pow(6)).toString()); + const priceMTMFor1USDC = new Big(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); + const priceInBaseUnits = new BN(new Big(priceMTMFor1USDC).times(new Big(10).pow(6)).toString()); - this.cachedQuotes.push(price); + this.cachedQuotes.push(priceInBaseUnits); if (this.cachedQuotes.length > 3) { this.cachedQuotes.shift(); } diff --git a/server.ts b/server.ts index 8ee34d4..86253c6 100644 --- a/server.ts +++ b/server.ts @@ -29,7 +29,6 @@ declare global { app.prepare().then(() => { const server = createServer(async (req, res) => { const parsedUrl = parse(req.url!, true); - (req as any).quotesService = quotesService; handle(req, res, parsedUrl); }); -- 2.45.2 From 2806fd344a110c4fce551848c81fb071d7aa0568 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 17:42:55 +0530 Subject: [PATCH 17/20] Update variable names in quotes service --- quotes-service.ts | 15 ++++++++------- server.ts | 5 ++--- src/app/api/flux/route.ts | 10 +++++----- src/app/api/quotes/route.ts | 6 +++--- src/components/AIServiceCard.tsx | 2 -- tsconfig.server.json | 1 + 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/quotes-service.ts b/quotes-service.ts index bb3d72a..780823b 100644 --- a/quotes-service.ts +++ b/quotes-service.ts @@ -10,7 +10,8 @@ const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; class QuotesService { - private cachedQuotes: BN[] = []; + // Stores the MTM amount for 1 USDC + private cachedMTMAmounts: BN[] = []; async fetchAndCacheQuotes(): Promise { try { @@ -25,19 +26,19 @@ class QuotesService { // Handle price with Big.js, then convert to BN const priceMTMFor1USDC = new Big(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); - const priceInBaseUnits = new BN(new Big(priceMTMFor1USDC).times(new Big(10).pow(6)).toString()); + const priceMTMFor1USDCInBaseUnits = new BN(new Big(priceMTMFor1USDC).times(new Big(10).pow(6)).toString()); - this.cachedQuotes.push(priceInBaseUnits); - if (this.cachedQuotes.length > 3) { - this.cachedQuotes.shift(); + this.cachedMTMAmounts.push(priceMTMFor1USDCInBaseUnits); + if (this.cachedMTMAmounts.length > 3) { + this.cachedMTMAmounts.shift(); } } catch (error) { console.error('Error fetching quotes:', error); } } - getQuotes(): BN[] { - return this.cachedQuotes; + getMTMAmountsFor1USDC(): BN[] { + return this.cachedMTMAmounts; } } diff --git a/server.ts b/server.ts index 86253c6..4789578 100644 --- a/server.ts +++ b/server.ts @@ -2,9 +2,8 @@ import { createServer } from 'http'; import { parse } from 'url'; import next from 'next'; -// Reference: https://www.dotenv.org/docs/quickstart#initial-setup -import dotenv from 'dotenv'; -dotenv.config(); +// Reference: https://github.com/motdotla/dotenv?tab=readme-ov-file#how-do-i-use-dotenv-with-import +import 'dotenv/config' import { QuotesService } from './quotes-service'; diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index 8b7601d..4773307 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -47,14 +47,14 @@ export async function POST(req: NextRequest): Promise { ) } - const quotes: BN[] = (global as any).quotesService.getQuotes(); - const lowestQuote = quotes.reduce((minQuote, currentQuote) => BN.min(minQuote, currentQuote), quotes[0]); + const amountOfMTM: BN[] = (global as any).quotesService.getMTMAmountsFor1USDC(); + const lowestAmountOfMTM = amountOfMTM.reduce((minQuote, currentQuote) => BN.min(minQuote, currentQuote), amountOfMTM[0]); const scale = new BN(100); - const scaledCost = new BN(model.cost * 100); + const scaledModelCost = new BN(model.cost).mul(scale); - const tokenAmount = scaledCost.mul(new BN(lowestQuote)).div(scale); - const isPaymentVerified = await verifyPayment(transactionSignature, tokenAmount); + const lowestTokenAmount = scaledModelCost.mul(new BN(lowestAmountOfMTM)).div(scale); + const isPaymentVerified = await verifyPayment(transactionSignature, lowestTokenAmount); if (!isPaymentVerified) { return NextResponse.json( diff --git a/src/app/api/quotes/route.ts b/src/app/api/quotes/route.ts index 8ef3744..24a5b07 100644 --- a/src/app/api/quotes/route.ts +++ b/src/app/api/quotes/route.ts @@ -3,9 +3,9 @@ import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { try { - const quotes: BN[] = (global as any).quotesService.getQuotes(); - const quotesAsString = quotes.map(quote => quote.toString()); - return NextResponse.json(quotesAsString); + const amountOfMTM: BN[] = (global as any).quotesService.getMTMAmountsFor1USDC(); + const amountOfMTMAsString = amountOfMTM.map(amount => amount.toString()); + return NextResponse.json(amountOfMTMAsString); } catch (error) { return NextResponse.json({ error: 'Failed to fetch quotes' }, { status: 500 }); } diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 547b8bd..57b8e00 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -20,8 +20,6 @@ interface GenerationState { } const formatBNWithDecimals = (value: BN, decimals: number): string => { - if (value.isZero()) return '0.' + '0'.repeat(decimals); // Handle zero case - const bigValue = new Big(value.toString()); const factor = new Big(10).pow(decimals); diff --git a/tsconfig.server.json b/tsconfig.server.json index 6a8dc0a..7e9180b 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -1,3 +1,4 @@ +// Reference: https://github.com/vercel/next.js/blob/canary/examples/custom-server/tsconfig.server.json { "extends": "./tsconfig.json", "compilerOptions": { -- 2.45.2 From 41470fc895d71b8027fde98a55236c7946d5d7b8 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 18:08:55 +0530 Subject: [PATCH 18/20] Update method to convert base units to decimal --- quotes-service.ts | 2 +- src/app/api/flux/route.ts | 4 ++-- src/app/page.tsx | 4 ++-- src/components/AIServiceCard.tsx | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/quotes-service.ts b/quotes-service.ts index 780823b..a16ad43 100644 --- a/quotes-service.ts +++ b/quotes-service.ts @@ -24,7 +24,7 @@ class QuotesService { const quoteResponse = await response.json(); - // Handle price with Big.js, then convert to BN + // Handle price with big.js, then convert to bn.js instance const priceMTMFor1USDC = new Big(quoteResponse['data'][USDC_MINT]['price']).toFixed(6); const priceMTMFor1USDCInBaseUnits = new BN(new Big(priceMTMFor1USDC).times(new Big(10).pow(6)).toString()); diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index 4773307..e26e4ab 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -53,8 +53,8 @@ export async function POST(req: NextRequest): Promise { const scale = new BN(100); const scaledModelCost = new BN(model.cost).mul(scale); - const lowestTokenAmount = scaledModelCost.mul(new BN(lowestAmountOfMTM)).div(scale); - const isPaymentVerified = await verifyPayment(transactionSignature, lowestTokenAmount); + const lowestTokenAmountForModel = scaledModelCost.mul(new BN(lowestAmountOfMTM)).div(scale); + const isPaymentVerified = await verifyPayment(transactionSignature, lowestTokenAmountForModel); if (!isPaymentVerified) { return NextResponse.json( diff --git a/src/app/page.tsx b/src/app/page.tsx index a147ef1..37adc67 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -121,8 +121,8 @@ const Page: React.FC = (): React.ReactElement => {
{FLUX_MODELS.map((model) => { // Convert cost from number to BN - const costBN = new BN(model.cost * 100); - const priceMTM = costBN.mul(priceMTMFor1USDC).div(new BN(100)); + const scaledModelCost = new BN(model.cost * 100); + const priceMTM = scaledModelCost.mul(priceMTMFor1USDC).div(new BN(100)); return ( { +const baseUnitToDecimalFormat = (value: BN, decimals: number): string => { const bigValue = new Big(value.toString()); const factor = new Big(10).pow(decimals); @@ -95,7 +95,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {priceMTM ? formatBNWithDecimals(priceMTM, 6) : '...'} MTM + Cost: {priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM
@@ -119,7 +119,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${priceMTM ? formatBNWithDecimals(priceMTM, 6) : '...'} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM & Generate`} -- 2.45.2 From 2f7fbac7038b30921cbd980309a43d22e9c9c10c Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 30 Jan 2025 19:04:26 +0530 Subject: [PATCH 19/20] Improve error handling in flux API --- server.ts | 9 +++++---- src/app/api/flux/route.ts | 7 +++++-- src/app/api/quotes/route.ts | 4 ++-- src/app/page.tsx | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/server.ts b/server.ts index 4789578..55da595 100644 --- a/server.ts +++ b/server.ts @@ -25,18 +25,19 @@ declare global { // Initialize global quotes service (global as any).quotesService = quotesService -app.prepare().then(() => { +app.prepare().then(async() => { const server = createServer(async (req, res) => { const parsedUrl = parse(req.url!, true); handle(req, res, parsedUrl); }); + await quotesService.fetchAndCacheQuotes(); // Initial store + server.listen(port, () => { console.log(`> Server listening at http://localhost:${port}`); }); - // Initial call and interval setup - quotesService.fetchAndCacheQuotes(); // Initial store - setInterval(() => quotesService.fetchAndCacheQuotes(), 5 * 60 * 1000); // Update cache every 5 minutes + // Interval setup + setInterval(async () => await quotesService.fetchAndCacheQuotes(), 5 * 60 * 1000); // Update cache every 5 minutes }); diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index e26e4ab..fc1cebf 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -91,14 +91,17 @@ export async function POST(req: NextRequest): Promise { if (!imageUrl) { console.error('No image URL in response:', result) - throw new Error('No image URL in response') + return NextResponse.json( + { error: 'No image URL in response: ', result }, + { status: 400 } + ) } return NextResponse.json({ imageUrl }) } catch (error) { console.error('Flux generation error:', error) return NextResponse.json( - { error: error instanceof Error ? error.message : 'Failed to generate image' }, + { error: 'Failed to generate image' }, { status: 500 } ) } diff --git a/src/app/api/quotes/route.ts b/src/app/api/quotes/route.ts index 24a5b07..71e9947 100644 --- a/src/app/api/quotes/route.ts +++ b/src/app/api/quotes/route.ts @@ -4,8 +4,8 @@ import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { try { const amountOfMTM: BN[] = (global as any).quotesService.getMTMAmountsFor1USDC(); - const amountOfMTMAsString = amountOfMTM.map(amount => amount.toString()); - return NextResponse.json(amountOfMTMAsString); + const latestMTMAmount = amountOfMTM[amountOfMTM.length - 1].toString(); + return NextResponse.json(latestMTMAmount); } catch (error) { return NextResponse.json({ error: 'Failed to fetch quotes' }, { status: 500 }); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 37adc67..889f59e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -26,7 +26,7 @@ const Page: React.FC = (): React.ReactElement => { const data = await response.json(); // Convert the string back to BN - const price = new BN(data[data.length - 1]); + const price = new BN(data); setPriceMTMFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); -- 2.45.2 From f19610c19f06e491e8843c4108bc38a74ae657d7 Mon Sep 17 00:00:00 2001 From: Adwait Gharpure Date: Thu, 30 Jan 2025 20:20:42 +0530 Subject: [PATCH 20/20] Return JSON in quotes API --- src/app/api/quotes/route.ts | 2 +- src/app/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/quotes/route.ts b/src/app/api/quotes/route.ts index 71e9947..2041d5a 100644 --- a/src/app/api/quotes/route.ts +++ b/src/app/api/quotes/route.ts @@ -5,7 +5,7 @@ export async function GET(req: NextRequest) { try { const amountOfMTM: BN[] = (global as any).quotesService.getMTMAmountsFor1USDC(); const latestMTMAmount = amountOfMTM[amountOfMTM.length - 1].toString(); - return NextResponse.json(latestMTMAmount); + return NextResponse.json({ latestMTMAmount }); } catch (error) { return NextResponse.json({ error: 'Failed to fetch quotes' }, { status: 500 }); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 889f59e..2ed258a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -26,7 +26,7 @@ const Page: React.FC = (): React.ReactElement => { const data = await response.json(); // Convert the string back to BN - const price = new BN(data); + const price = new BN(data.latestMTMAmount); setPriceMTMFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); -- 2.45.2