forked from mito-systems/sol-mem-gen
Use bigint for handling token price
This commit is contained in:
parent
36a140734a
commit
8b5dbb46fa
@ -3,7 +3,8 @@ import { fal } from "@fal-ai/client"
|
|||||||
import { FLUX_MODELS } from '../../../services/fluxService'
|
import { FLUX_MODELS } from '../../../services/fluxService'
|
||||||
import { initializeDataSource } from '../../../data-source'
|
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) {
|
if (!process.env.FAL_AI_KEY) {
|
||||||
throw new Error('FAL_AI_KEY is not configured in environment variables')
|
throw new Error('FAL_AI_KEY is not configured in environment variables')
|
||||||
@ -46,7 +47,13 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount)
|
||||||
|
|
||||||
if (!isPaymentVerified) {
|
if (!isPaymentVerified) {
|
||||||
|
|||||||
@ -15,14 +15,13 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
type: null
|
type: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const [usdcPrice, setUsdcPrice] = useState<number>(0);
|
const [MTMFor1USDC, setMTMFor1USDC] = useState<bigint>(BigInt(0));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
const fetchPrice = async () => {
|
const fetchPrice = async () => {
|
||||||
try {
|
try {
|
||||||
const price = await getUSDCToMTMQuote();
|
const price = await getUSDCToMTMQuote();
|
||||||
setUsdcPrice(price);
|
setMTMFor1USDC(price);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch price:', error);
|
console.error('Failed to fetch price:', error);
|
||||||
}
|
}
|
||||||
@ -55,24 +54,26 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
connected: false,
|
connected: false,
|
||||||
publicKey: null,
|
publicKey: null,
|
||||||
type: null
|
type: null
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFluxGeneration = (modelId: string, cost: number) => {
|
const handleFluxGeneration = (modelId: string, cost: bigint) => {
|
||||||
return async (prompt: string): Promise<FluxGenerationResult> => {
|
return async (prompt: string): Promise<FluxGenerationResult> => {
|
||||||
const type = walletState.type;
|
const { connected, publicKey, type } = walletState;
|
||||||
if (!walletState.connected || !walletState.publicKey || !walletState.type ||
|
|
||||||
|
if (!connected || !publicKey || !type ||
|
||||||
(type === 'phantom' && !window.phantom) ||
|
(type === 'phantom' && !window.phantom) ||
|
||||||
(type === 'solflare' && !window.solflare)) {
|
(type === 'solflare' && !window.solflare)) {
|
||||||
return { error: 'Wallet not connected' }
|
return { error: 'Wallet not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process payment first
|
try {
|
||||||
|
// Convert cost in USDC to MTM tokens using the price ratio
|
||||||
const paymentResult = await processMTMPayment(
|
const paymentResult = await processMTMPayment(
|
||||||
walletState.publicKey,
|
publicKey,
|
||||||
cost,
|
cost,
|
||||||
walletState.type
|
type
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!paymentResult.success) {
|
if (!paymentResult.success) {
|
||||||
@ -85,8 +86,12 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
return { error: 'Transaction signature not found' }
|
return { error: 'Transaction signature not found' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then generate image with specified model
|
// Generate image with specified model and transaction reference
|
||||||
return generateWithFlux(prompt, modelId, transactionSignature)
|
return generateWithFlux(prompt, modelId, transactionSignature)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleFluxGeneration:', error)
|
||||||
|
return { error: error instanceof Error ? error.message : 'Unknown error' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,10 +121,23 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
key={model.modelId}
|
key={model.modelId}
|
||||||
title={model.name}
|
title={model.name}
|
||||||
description={model.description}
|
description={model.description}
|
||||||
tokenCost={model.cost}
|
|
||||||
isWalletConnected={walletState.connected}
|
isWalletConnected={walletState.connected}
|
||||||
onGenerate={handleFluxGeneration(model.modelId, model.cost)}
|
// TODO: Use bn.js for better precision
|
||||||
mtmPrice={model.cost * usdcPrice}
|
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;
|
||||||
|
})()}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,9 @@ import React, { useState } from 'react'
|
|||||||
interface AIServiceCardProps {
|
interface AIServiceCardProps {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
tokenCost: number
|
|
||||||
isWalletConnected: boolean
|
isWalletConnected: boolean
|
||||||
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
|
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
|
||||||
mtmPrice: number | null
|
mtmPrice: bigint
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GenerationState {
|
interface GenerationState {
|
||||||
@ -21,7 +20,6 @@ interface GenerationState {
|
|||||||
const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
tokenCost,
|
|
||||||
isWalletConnected,
|
isWalletConnected,
|
||||||
onGenerate,
|
onGenerate,
|
||||||
mtmPrice
|
mtmPrice
|
||||||
@ -83,7 +81,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 mt-2">{description}</p>
|
<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">
|
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
|
||||||
Cost: {mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM
|
Cost: {mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -107,7 +105,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
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`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -16,19 +16,19 @@ export const FLUX_MODELS: FluxModelConfig[] = [
|
|||||||
modelId: "fal-ai/flux/schnell",
|
modelId: "fal-ai/flux/schnell",
|
||||||
name: "Schnell",
|
name: "Schnell",
|
||||||
description: "Fast meme generator",
|
description: "Fast meme generator",
|
||||||
cost: 0.1
|
cost: 0.05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
modelId: "fal-ai/recraft-v3",
|
modelId: "fal-ai/recraft-v3",
|
||||||
name: "Recraft",
|
name: "Recraft",
|
||||||
description: "Advanced meme generator",
|
description: "Advanced meme generator",
|
||||||
cost: 0.15
|
cost: 0.10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
modelId: "fal-ai/stable-diffusion-v35-large",
|
modelId: "fal-ai/stable-diffusion-v35-large",
|
||||||
name: "Marquee",
|
name: "Marquee",
|
||||||
description: "Best meme generator",
|
description: "Best meme generator",
|
||||||
cost: 0.2
|
cost: 0.15
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -60,8 +60,10 @@ export async function getUSDCToMTMQuote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const quoteResponse = await response.json();
|
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;
|
return price;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +73,7 @@ interface WalletAdapter {
|
|||||||
|
|
||||||
export async function processMTMPayment(
|
export async function processMTMPayment(
|
||||||
walletPublicKey: string,
|
walletPublicKey: string,
|
||||||
tokenAmount: number,
|
tokenAmount: bigint,
|
||||||
walletType: WalletType
|
walletType: WalletType
|
||||||
): Promise<PaymentResult> {
|
): Promise<PaymentResult> {
|
||||||
try {
|
try {
|
||||||
@ -143,12 +145,14 @@ export async function processMTMPayment(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tokens = tokenAmount / BigInt(10 ** 6);
|
||||||
|
|
||||||
transaction.add(
|
transaction.add(
|
||||||
createTransferInstruction(
|
createTransferInstruction(
|
||||||
senderATA,
|
senderATA,
|
||||||
receiverATA,
|
receiverATA,
|
||||||
senderPublicKey,
|
senderPublicKey,
|
||||||
BigInt(tokenAmount * (10 ** 6))
|
tokens * BigInt(10 ** 6)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,7 +161,7 @@ export async function processMTMPayment(
|
|||||||
transaction.feePayer = senderPublicKey
|
transaction.feePayer = senderPublicKey
|
||||||
|
|
||||||
console.log('Sending transaction...')
|
console.log('Sending transaction...')
|
||||||
const { signature } = await wallet.signAndSendTransaction(transaction)
|
const { signature } = await wallet.signAndSendTransaction(transaction);
|
||||||
console.log('Transaction sent:', signature)
|
console.log('Transaction sent:', signature)
|
||||||
|
|
||||||
const confirmation = await connection.confirmTransaction({
|
const confirmation = await connection.confirmTransaction({
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { Connection } from '@solana/web3.js';
|
import { Connection } from '@solana/web3.js';
|
||||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
|
||||||
import { AppDataSource } from '../data-source';
|
import { AppDataSource } from '../data-source';
|
||||||
import { Payment } from '../entity/Payment';
|
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_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||||
const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_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(
|
export async function verifyPayment(
|
||||||
transactionSignature: string,
|
transactionSignature: string,
|
||||||
expectedAmount: number,
|
expectedAmount: bigint,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Check if the signature is already used
|
// Check if the signature is already used
|
||||||
@ -67,7 +71,14 @@ export async function verifyPayment(
|
|||||||
const { info } = parsed;
|
const { info } = parsed;
|
||||||
const { amount } = info;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user