Use MTM price based on USDC for meme generation (#7)

Part of https://www.notion.so/Use-MTM-price-based-on-pool-189a6b22d47280ff9373c89f40b378d1

Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-authored-by: Adwait Gharpure <adwaitgharpure@gmail.com>
Reviewed-on: #7
Co-authored-by: adwait <adwait@noreply.git.vdb.to>
Co-committed-by: adwait <adwait@noreply.git.vdb.to>
This commit was merged in pull request #7.
This commit is contained in:
authored and nabarun committed 2025-01-30 15:03:09 +00:00
1 parent fa6a88f297
commit 36f298ff96
14 files changed
+440 -55

No files matched your search

+16 -6
View File
@@ -1,9 +1,10 @@
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'
import { verifyPayment, isSignatureUsed, markSignatureAsUsed } from '../../../utils/verifyPayment'
import { verifyPayment, markSignatureAsUsed } from '../../../utils/verifyPayment';
if (!process.env.FAL_AI_KEY) {
throw new Error('FAL_AI_KEY is not configured in environment variables')
@@ -46,8 +47,14 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
)
}
const expectedAmount = model.cost;
const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount)
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 scaledModelCost = new BN(model.cost).mul(scale);
const lowestTokenAmountForModel = scaledModelCost.mul(new BN(lowestAmountOfMTM)).div(scale);
const isPaymentVerified = await verifyPayment(transactionSignature, lowestTokenAmountForModel);
if (!isPaymentVerified) {
return NextResponse.json(
@@ -84,14 +91,17 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
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 }
)
}
+12
View File
@@ -0,0 +1,12 @@
import BN from 'bn.js';
import { NextRequest, NextResponse } from 'next/server';
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 });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch quotes' }, { status: 500 });
}
}
+68 -29
View File
@@ -1,6 +1,8 @@
'use client'
import React, { useState } from 'react'
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,6 +17,25 @@ const Page: React.FC = (): React.ReactElement => {
type: null
})
const [priceMTMFor1USDC, setPriceMTMFor1USDC] = useState<BN>(new BN(0));
useEffect(() => {
const fetchPrice = async () => {
try {
const response = await fetch('/api/quotes');
const data = await response.json();
// Convert the string back to BN
const price = new BN(data.latestMTMAmount);
setPriceMTMFor1USDC(price);
} catch (error) {
console.error('Failed to fetch price:', error);
}
};
fetchPrice();
}, []);
const handleConnect = async (walletType: WalletType): Promise<void> => {
try {
const newWalletState = await connectWallet(walletType)
@@ -40,30 +61,40 @@ const Page: React.FC = (): React.ReactElement => {
}
}
const handleFluxGeneration = (modelId: string, cost: number) => {
const handleFluxGeneration = (modelId: string, cost: BN) => {
return async (prompt: string): Promise<FluxGenerationResult> => {
const type = walletState.type;
if (!walletState.connected || !walletState.publicKey ||
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;
// Then generate image with specified model
return generateWithFlux(prompt, modelId, transactionSignature)
}
}
@@ -83,22 +114,30 @@ const Page: React.FC = (): React.ReactElement => {
walletState={walletState}
onConnect={handleConnect}
onDisconnect={handleDisconnect}
/>
/>
</div>
{/* Flux Models Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{FLUX_MODELS.map((model) => (
<AIServiceCard
key={model.modelId}
title={model.name}
description={model.description}
tokenCost={model.cost}
isWalletConnected={walletState.connected}
onGenerate={handleFluxGeneration(model.modelId, model.cost)}
/>
))}
{FLUX_MODELS.map((model) => {
// Convert cost from number to BN
const scaledModelCost = new BN(model.cost * 100);
const priceMTM = scaledModelCost.mul(priceMTMFor1USDC).div(new BN(100));
return (
<AIServiceCard
key={model.modelId}
title={model.name}
description={model.description}
isWalletConnected={walletState.connected}
onGenerate={handleFluxGeneration(
model.modelId,
priceMTM
)}
priceMTM={priceMTM}
/>
);
})}
{/* Coming Soon Card */}
<div className="relative bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-yellow-500/10 to-orange-500/10 opacity-50"></div>
+19 -5
View File
@@ -1,13 +1,15 @@
'use client'
import React, { useState } from 'react'
import BN from 'bn.js';
import Big from 'big.js';
interface AIServiceCardProps {
title: string
description: string
tokenCost: number
isWalletConnected: boolean
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
priceMTM: BN
}
interface GenerationState {
@@ -17,12 +19,19 @@ interface GenerationState {
error: string | null
}
const baseUnitToDecimalFormat = (value: BN, decimals: number): string => {
const bigValue = new Big(value.toString());
const factor = new Big(10).pow(decimals);
return bigValue.div(factor).toFixed(decimals);
}
const AIServiceCard: React.FC<AIServiceCardProps> = ({
title,
description,
tokenCost,
isWalletConnected,
onGenerate
onGenerate,
priceMTM
}) => {
const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({
@@ -50,6 +59,11 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
loading: false,
error: result.error,
})
// Reload the page to get latest prices
setTimeout(() => {
window.location.reload();
}, 3000);
return
}
@@ -81,7 +95,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
</h2>
<p className="text-gray-400 mt-2">{description}</p>
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
Cost: {tokenCost} MTM
Cost: {priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM
</div>
</div>
@@ -105,7 +119,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
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 ${priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM & Generate`}
</button>
</div>
+4 -4
View File
@@ -16,19 +16,19 @@ export const FLUX_MODELS: FluxModelConfig[] = [
modelId: "fal-ai/flux/schnell",
name: "Schnell",
description: "Fast meme generator",
cost: 300
cost: 0.05
},
{
modelId: "fal-ai/recraft-v3",
name: "Recraft",
description: "Advanced meme generator",
cost: 400
cost: 0.10
},
{
modelId: "fal-ai/stable-diffusion-v35-large",
name: "Marquee",
description: "Best meme generator",
cost: 500
cost: 0.15
}
]
@@ -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()
+12 -2
View File
@@ -1,3 +1,6 @@
import assert from 'assert';
import BN from 'bn.js';
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import {
TOKEN_PROGRAM_ID,
@@ -5,8 +8,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;
@@ -47,7 +55,7 @@ interface WalletAdapter {
export async function processMTMPayment(
walletPublicKey: string,
tokenAmount: number,
tokenAmount: BN,
walletType: WalletType
): Promise<PaymentResult> {
try {
@@ -119,12 +127,14 @@ export async function processMTMPayment(
)
}
const amountToSend = BigInt(tokenAmount.toString());
transaction.add(
createTransferInstruction(
senderATA,
receiverATA,
senderPublicKey,
BigInt(tokenAmount * (10 ** 6))
amountToSend
)
)
+10 -3
View File
@@ -1,9 +1,14 @@
import assert from 'assert';
import BN from 'bn.js';
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 +47,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise
export async function verifyPayment(
transactionSignature: string,
expectedAmount: number,
tokenAmount: BN,
): Promise<boolean> {
try {
// Check if the signature is already used
@@ -67,10 +72,12 @@ export async function verifyPayment(
const { info } = parsed;
const { amount } = info;
if (BigInt(amount) === BigInt(expectedAmount * (10 ** 6))) {
const transactionAmount = new BN(amount);
if (transactionAmount.gte(tokenAmount)) {
return true;
}
console.log('Transaction amount is less than minimum amount. Rejecting request');
return false;
} catch (error) {
console.error('Verification error:', error);