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/.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/package-lock.json b/package-lock.json index d46cd39..127b13e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "@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", "openai": "^4.77.0", "react": "^18", @@ -20,6 +23,7 @@ "typeorm": "^0.3.12" }, "devDependencies": { + "@types/bn.js": "^5.1.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -28,6 +32,7 @@ "eslint-config-next": "13.5.4", "postcss": "^8", "tailwindcss": "^3", + "ts-node": "^10.9.2", "typescript": "^5" } }, @@ -54,6 +59,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", @@ -279,7 +308,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" } @@ -297,7 +326,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", @@ -790,6 +819,44 @@ "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", + "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", @@ -988,7 +1055,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" }, @@ -1005,6 +1072,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", @@ -1422,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", @@ -2051,6 +2144,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", @@ -2266,6 +2366,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", @@ -4646,6 +4756,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", @@ -7073,6 +7190,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", @@ -7505,6 +7673,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", @@ -7864,6 +8039,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 c1eeba9..48b3908 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": { @@ -13,6 +13,9 @@ "@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", "openai": "^4.77.0", "react": "^18", @@ -21,6 +24,7 @@ "typeorm": "^0.3.12" }, "devDependencies": { + "@types/bn.js": "^5.1.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -29,6 +33,7 @@ "eslint-config-next": "13.5.4", "postcss": "^8", "tailwindcss": "^3", + "ts-node": "^10.9.2", "typescript": "^5" } } diff --git a/quotes-service.ts b/quotes-service.ts new file mode 100644 index 0000000..a16ad43 --- /dev/null +++ b/quotes-service.ts @@ -0,0 +1,45 @@ +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'); + +const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT; +const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT; + +class QuotesService { + // Stores the MTM amount for 1 USDC + private cachedMTMAmounts: 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(); + + // 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()); + + this.cachedMTMAmounts.push(priceMTMFor1USDCInBaseUnits); + if (this.cachedMTMAmounts.length > 3) { + this.cachedMTMAmounts.shift(); + } + } catch (error) { + console.error('Error fetching quotes:', error); + } + } + + getMTMAmountsFor1USDC(): BN[] { + return this.cachedMTMAmounts; + } +} + +export { QuotesService }; diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..55da595 --- /dev/null +++ b/server.ts @@ -0,0 +1,43 @@ +import { createServer } from 'http'; +import { parse } from 'url'; +import next from 'next'; + +// 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'; + +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 { + quotesService: typeof quotesService + } + } +} + +// TODO: Look for a better way to use quotesService +// Initialize global quotes service +(global as any).quotesService = quotesService + +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}`); + }); + + // 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 e7b1000..fc1cebf 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -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 { ) } - 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 { 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 new file mode 100644 index 0000000..2041d5a --- /dev/null +++ b/src/app/api/quotes/route.ts @@ -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 }); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 933555b..2ed258a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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(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 => { 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 => { - 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} - /> + /> {/* Flux Models Grid */}
- {FLUX_MODELS.map((model) => ( - - ))} + {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 ( + + ); + })} {/* Coming Soon Card */}
diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index c078973..12ef008 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -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 = ({ title, description, - tokenCost, isWalletConnected, - onGenerate + onGenerate, + priceMTM }) => { const [inputText, setInputText] = useState('') const [generationState, setGenerationState] = useState({ @@ -50,6 +59,11 @@ const AIServiceCard: React.FC = ({ 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 = ({

{description}

- Cost: {tokenCost} MTM + Cost: {priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM
@@ -105,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 ${tokenCost} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM & Generate`}
diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index 7f22c2e..14c68a0 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.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() diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index a966b7a..6e809a2 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -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 { 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 ) ) diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 770a984..2b5f91a 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -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 { 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); diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..7e9180b --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,13 @@ +// Reference: https://github.com/vercel/next.js/blob/canary/examples/custom-server/tsconfig.server.json +{ + "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