Use MTM price based on USDC for meme generation #7

Merged
nabarun merged 20 commits from ag-usd-price into main 2025-01-30 15:03:09 +00:00
14 changed files with 440 additions and 55 deletions

View File

@ -4,3 +4,4 @@ NEXT_PUBLIC_MTM_TOKEN_MINT=97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump
NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY
NEXT_PUBLIC_SOLANA_RPC_URL=https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4 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_SOLANA_WEBSOCKET_URL=wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4
NEXT_PUBLIC_USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ node_modules
.env.test .env.test
database.sqlite database.sqlite
dist

191
package-lock.json generated
View File

@ -12,6 +12,9 @@
"@google/generative-ai": "^0.21.0", "@google/generative-ai": "^0.21.0",
"@solana/spl-token": "^0.3.8", "@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.4", "@solana/web3.js": "^1.78.4",
"big.js": "^6.2.2",
"bn.js": "^5.2.0",
"dotenv": "^16.4.7",
"next": "13.5.4", "next": "13.5.4",
"openai": "^4.77.0", "openai": "^4.77.0",
"react": "^18", "react": "^18",
@ -20,6 +23,7 @@
"typeorm": "^0.3.12" "typeorm": "^0.3.12"
}, },
"devDependencies": { "devDependencies": {
"@types/bn.js": "^5.1.6",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
@ -28,6 +32,7 @@
"eslint-config-next": "13.5.4", "eslint-config-next": "13.5.4",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3", "tailwindcss": "^3",
"ts-node": "^10.9.2",
"typescript": "^5" "typescript": "^5"
} }
}, },
@ -54,6 +59,30 @@
"node": ">=6.9.0" "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": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
@ -279,7 +308,7 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true, "devOptional": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
@ -297,7 +326,7 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true "devOptional": true
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25", "version": "0.3.25",
@ -790,6 +819,44 @@
"node": ">= 6" "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": { "node_modules/@types/connect": {
"version": "3.4.38", "version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@ -988,7 +1055,7 @@
"version": "8.14.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -1005,6 +1072,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "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": { "node_modules/agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "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": { "node_modules/bigint-buffer": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
@ -2051,6 +2144,13 @@
"license": "ISC", "license": "ISC",
"optional": true "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2266,6 +2366,16 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true "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": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" "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": { "node_modules/make-fetch-happen": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "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==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true "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": { "node_modules/tsconfig-paths": {
"version": "3.15.0", "version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@ -7505,6 +7673,13 @@
"uuid": "dist/bin/uuid" "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": { "node_modules/watchpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@ -7864,6 +8039,16 @@
"node": ">=8" "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": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -3,9 +3,9 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "ts-node --project tsconfig.server.json server.ts",
"build": "next build", "build": "next build && tsc --project tsconfig.server.json",
"start": "next start", "start": "NODE_ENV=production node dist/server.js",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
@ -13,6 +13,9 @@
"@google/generative-ai": "^0.21.0", "@google/generative-ai": "^0.21.0",
"@solana/spl-token": "^0.3.8", "@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.4", "@solana/web3.js": "^1.78.4",
"big.js": "^6.2.2",
"bn.js": "^5.2.0",
"dotenv": "^16.4.7",
"next": "13.5.4", "next": "13.5.4",
"openai": "^4.77.0", "openai": "^4.77.0",
"react": "^18", "react": "^18",
@ -21,6 +24,7 @@
"typeorm": "^0.3.12" "typeorm": "^0.3.12"
}, },
"devDependencies": { "devDependencies": {
"@types/bn.js": "^5.1.6",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
@ -29,6 +33,7 @@
"eslint-config-next": "13.5.4", "eslint-config-next": "13.5.4",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3", "tailwindcss": "^3",
"ts-node": "^10.9.2",
"typescript": "^5" "typescript": "^5"
} }
} }

45
quotes-service.ts Normal file
View File

@ -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<void> {
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 };

43
server.ts Normal file
View File

@ -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
});

View File

@ -1,9 +1,10 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import BN from 'bn.js';
import { fal } from "@fal-ai/client" 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, markSignatureAsUsed } from '../../../utils/verifyPayment';
import { verifyPayment, isSignatureUsed, markSignatureAsUsed } from '../../../utils/verifyPayment'
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,8 +47,14 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
) )
} }
const expectedAmount = model.cost; const amountOfMTM: BN[] = (global as any).quotesService.getMTMAmountsFor1USDC();
const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount) 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) { if (!isPaymentVerified) {
return NextResponse.json( return NextResponse.json(
@ -84,14 +91,17 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
if (!imageUrl) { if (!imageUrl) {
console.error('No image URL in response:', result) 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 }) return NextResponse.json({ imageUrl })
} catch (error) { } catch (error) {
console.error('Flux generation error:', error) console.error('Flux generation error:', error)
return NextResponse.json( return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to generate image' }, { error: 'Failed to generate image' },
{ status: 500 } { status: 500 }
) )
} }

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 });
}
}

View File

@ -1,6 +1,8 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState, useEffect } from 'react'
import BN from 'bn.js';
import WalletHeader from '../components/WalletHeader' import WalletHeader from '../components/WalletHeader'
import AIServiceCard from '../components/AIServiceCard' import AIServiceCard from '../components/AIServiceCard'
import { generateWithFlux, FluxGenerationResult, FLUX_MODELS } from '../services/fluxService' import { generateWithFlux, FluxGenerationResult, FLUX_MODELS } from '../services/fluxService'
@ -15,6 +17,25 @@ const Page: React.FC = (): React.ReactElement => {
type: null 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> => { const handleConnect = async (walletType: WalletType): Promise<void> => {
try { try {
const newWalletState = await connectWallet(walletType) const newWalletState = await connectWallet(walletType)
@ -40,20 +61,22 @@ const Page: React.FC = (): React.ReactElement => {
} }
} }
const handleFluxGeneration = (modelId: string, cost: number) => { const handleFluxGeneration = (modelId: string, cost: BN) => {
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 ||
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) {
@ -62,8 +85,16 @@ const Page: React.FC = (): React.ReactElement => {
const transactionSignature = paymentResult.transactionSignature; const transactionSignature = paymentResult.transactionSignature;
// Then generate image with specified model if (!transactionSignature) {
return { error: 'Transaction signature not found' }
}
// 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' }
}
} }
} }
@ -88,17 +119,25 @@ const Page: React.FC = (): React.ReactElement => {
{/* Flux Models Grid */} {/* Flux Models Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{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 (
<AIServiceCard <AIServiceCard
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)} onGenerate={handleFluxGeneration(
model.modelId,
priceMTM
)}
priceMTM={priceMTM}
/> />
))} );
})}
{/* Coming Soon Card */} {/* 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="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> <div className="absolute inset-0 bg-gradient-to-br from-yellow-500/10 to-orange-500/10 opacity-50"></div>

View File

@ -1,13 +1,15 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState } from 'react'
import BN from 'bn.js';
import Big from 'big.js';
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 }>
priceMTM: BN
} }
interface GenerationState { interface GenerationState {
@ -17,12 +19,19 @@ interface GenerationState {
error: string | null 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> = ({ const AIServiceCard: React.FC<AIServiceCardProps> = ({
title, title,
description, description,
tokenCost,
isWalletConnected, isWalletConnected,
onGenerate onGenerate,
priceMTM
}) => { }) => {
const [inputText, setInputText] = useState<string>('') const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({ const [generationState, setGenerationState] = useState<GenerationState>({
@ -50,6 +59,11 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
loading: false, loading: false,
error: result.error, error: result.error,
}) })
// Reload the page to get latest prices
setTimeout(() => {
window.location.reload();
}, 3000);
return return
} }
@ -81,7 +95,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: {tokenCost} MTM Cost: {priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM
</div> </div>
</div> </div>
@ -105,7 +119,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 ${tokenCost} MTM & Generate`} {generationState.loading ? 'Processing...' : `Pay ${priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM & Generate`}
</button> </button>
</div> </div>

View File

@ -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: 300 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: 400 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: 500 cost: 0.15
} }
] ]
@ -51,7 +51,7 @@ export async function generateWithFlux(
}) })
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to generate image') throw new Error('Failed to generate image, reloading...')
} }
const data = await response.json() const data = await response.json()

View File

@ -1,3 +1,6 @@
import assert from 'assert';
import BN from 'bn.js';
import { Connection, PublicKey, Transaction } from '@solana/web3.js' import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import { import {
TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID,
@ -5,8 +8,13 @@ import {
createAssociatedTokenAccountInstruction, createAssociatedTokenAccountInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID ASSOCIATED_TOKEN_PROGRAM_ID
} from '@solana/spl-token' } from '@solana/spl-token'
import { WalletType } from './types' 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 MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT;
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS; const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_PAYMENT_RECEIVER_ADDRESS;
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
@ -47,7 +55,7 @@ interface WalletAdapter {
export async function processMTMPayment( export async function processMTMPayment(
walletPublicKey: string, walletPublicKey: string,
tokenAmount: number, tokenAmount: BN,
walletType: WalletType walletType: WalletType
): Promise<PaymentResult> { ): Promise<PaymentResult> {
try { try {
@ -119,12 +127,14 @@ export async function processMTMPayment(
) )
} }
const amountToSend = BigInt(tokenAmount.toString());
transaction.add( transaction.add(
createTransferInstruction( createTransferInstruction(
senderATA, senderATA,
receiverATA, receiverATA,
senderPublicKey, senderPublicKey,
BigInt(tokenAmount * (10 ** 6)) amountToSend
) )
) )

View File

@ -1,9 +1,14 @@
import assert from 'assert';
import BN from 'bn.js';
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 +47,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise
export async function verifyPayment( export async function verifyPayment(
transactionSignature: string, transactionSignature: string,
expectedAmount: number, tokenAmount: BN,
): Promise<boolean> { ): Promise<boolean> {
try { try {
// Check if the signature is already used // Check if the signature is already used
@ -67,10 +72,12 @@ 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 = new BN(amount);
if (transactionAmount.gte(tokenAmount)) {
return true; return true;
} }
console.log('Transaction amount is less than minimum amount. Rejecting request');
return false; return false;
} catch (error) { } catch (error) {
console.error('Verification error:', error); console.error('Verification error:', error);

13
tsconfig.server.json Normal file
View File

@ -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"]
}