forked from mito-systems/sol-mem-gen
Create WSOL lock for every fourth verified tweet #12
19
.env.example
19
.env.example
@ -1,11 +1,11 @@
|
||||
# Get key from https://fal.ai
|
||||
FAL_AI_KEY=
|
||||
|
||||
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
|
||||
NEXT_PUBLIC_MTM_MINT_ADDRESS=97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump
|
||||
NEXT_PUBLIC_MTM_RECIPIENT_MULTISIG_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY
|
||||
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
||||
NEXT_PUBLIC_SOLANA_WEBSOCKET_URL=wss://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
||||
NEXT_PUBLIC_USDC_MINT_ADDRESS=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
|
||||
|
||||
# Generate a key at https://app.pinata.cloud/developers/api-keys
|
||||
PINATA_JWT=
|
||||
@ -16,4 +16,11 @@ PINATA_GATEWAY=
|
||||
# Change to your website URL
|
||||
# For development: SITE_URL=http://localhost:3000
|
||||
SITE_URL=https://memes.markto.market
|
||||
NEXT_PUBLIC_ACCOUNT_HANDLE=
|
||||
NEXT_PUBLIC_TWITTER_HANDLE=
|
||||
|
||||
WSOL_LOCKER_ACCOUNT_PK=
|
||||
WSOL_MINT_ADDRESS=So11111111111111111111111111111111111111112
|
||||
|
||||
# Duration in seconds that WSOL will be locked for
|
||||
WSOL_LOCK_DURATION_IN_SECONDS=172800 # 48 hours
|
||||
REWARD_MULTIPLIER=4
|
||||
|
2
Anchor.toml
Normal file
2
Anchor.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[provider]
|
||||
cluster = "mainnet"
|
@ -52,7 +52,7 @@ This project is a Solana-based meme generator that allows users to connect their
|
||||
PINATA_GATEWAY=
|
||||
|
||||
# Add the account handle to be set in the tweet
|
||||
NEXT_PUBLIC_ACCOUNT_HANDLE=
|
||||
NEXT_PUBLIC_TWITTER_HANDLE=
|
||||
```
|
||||
|
||||
- Run the development server:
|
||||
|
1
next-env.d.ts
vendored
1
next-env.d.ts
vendored
@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
162
package-lock.json
generated
162
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "solana-meme-generator",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.30.1",
|
||||
"@fal-ai/client": "^1.2.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@solana/spl-token": "^0.3.8",
|
||||
@ -24,7 +25,9 @@
|
||||
"typeorm": "^0.3.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/big.js": "^6.2.2",
|
||||
"@types/bn.js": "^5.1.6",
|
||||
"@types/bs58": "^4.0.4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
@ -73,6 +76,64 @@
|
||||
"@chainsafe/is-ip": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@coral-xyz/anchor": {
|
||||
"version": "0.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.30.1.tgz",
|
||||
"integrity": "sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ==",
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor-errors": "^0.30.1",
|
||||
"@coral-xyz/borsh": "^0.30.1",
|
||||
"@noble/hashes": "^1.3.1",
|
||||
"@solana/web3.js": "^1.68.0",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.2",
|
||||
"camelcase": "^6.3.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"crypto-hash": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"pako": "^2.0.3",
|
||||
"snake-case": "^3.0.4",
|
||||
"superstruct": "^0.15.4",
|
||||
"toml": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=11"
|
||||
}
|
||||
},
|
||||
"node_modules/@coral-xyz/anchor-errors": {
|
||||
"version": "0.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz",
|
||||
"integrity": "sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@coral-xyz/anchor/node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
},
|
||||
"node_modules/@coral-xyz/anchor/node_modules/superstruct": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz",
|
||||
"integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ=="
|
||||
},
|
||||
"node_modules/@coral-xyz/borsh": {
|
||||
"version": "0.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.30.1.tgz",
|
||||
"integrity": "sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ==",
|
||||
"dependencies": {
|
||||
"bn.js": "^5.1.2",
|
||||
"buffer-layout": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "^1.68.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",
|
||||
@ -878,6 +939,13 @@
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/big.js": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz",
|
||||
"integrity": "sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==",
|
||||
"dev": 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",
|
||||
@ -887,6 +955,17 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bs58": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.4.tgz",
|
||||
"integrity": "sha512-0IEpMFXXQi2zXaXl9GJ3sRwQo0uEkD+yFOv+FnAU5lkPtcu6h61xb7jc2CFPEZ5BUOaiP13ThuGc9HD4R8lR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"base-x": "^3.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
@ -1763,6 +1842,14 @@
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-layout": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz",
|
||||
"integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==",
|
||||
"engines": {
|
||||
"node": ">=4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
|
||||
@ -1872,6 +1959,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase-css": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
@ -2125,6 +2223,14 @@
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
||||
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -2138,6 +2244,17 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-hash": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-hash/-/crypto-hash-1.3.0.tgz",
|
||||
"integrity": "sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@ -2393,6 +2510,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||
"dependencies": {
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
@ -4714,6 +4840,14 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lower-case": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@ -5073,6 +5207,15 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/no-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||
"dependencies": {
|
||||
"lower-case": "^2.0.2",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.74.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||
@ -5498,6 +5641,11 @@
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@ -6551,6 +6699,15 @@
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/snake-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
|
||||
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
|
||||
"dependencies": {
|
||||
"dot-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
@ -7112,6 +7269,11 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
|
@ -9,9 +9,10 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.30.1",
|
||||
"@fal-ai/client": "^1.2.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@solana/spl-token": "^0.3.8",
|
||||
"@solana/spl-token": "^0.3.11",
|
||||
"@solana/web3.js": "^1.78.4",
|
||||
"big.js": "^6.2.2",
|
||||
"bn.js": "^5.2.0",
|
||||
@ -25,7 +26,9 @@
|
||||
"typeorm": "^0.3.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/big.js": "^6.2.2",
|
||||
"@types/bn.js": "^5.1.6",
|
||||
"@types/bs58": "^4.0.4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
@ -3,11 +3,11 @@ 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');
|
||||
assert(process.env.NEXT_PUBLIC_USDC_MINT_ADDRESS, 'USDC_MINT_ADDRESS is required');
|
||||
assert(process.env.NEXT_PUBLIC_MTM_MINT_ADDRESS, 'MTM_MINT_ADDRESS is required');
|
||||
|
||||
const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT;
|
||||
const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT;
|
||||
const MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_MINT_ADDRESS;
|
||||
const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT_ADDRESS;
|
||||
|
||||
class QuotesService {
|
||||
// Stores the MTM amount for 1 USDC
|
||||
|
23
src/app/api/lock/route.ts
Normal file
23
src/app/api/lock/route.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { createRewardLock } from "../../../utils/create-lock";
|
||||
import { extractTxInfo } from "../../../utils/extractTxInfo";
|
||||
|
||||
// export async function GET(req: NextRequest) {
|
||||
// try {
|
||||
// const { searchParams } = new URL(req.url);
|
||||
// const signature = searchParams.get('signature')
|
||||
|
||||
// const { authority, amount } = await extractTxInfo(signature);
|
||||
// if (!authority || Number(amount) <= 0) {
|
||||
// return NextResponse.json({ error: "Invalid transaction details" }, { status: 400 });
|
||||
// }
|
||||
|
||||
// const escrow = await createRewardLock(authority, amount);
|
||||
// return NextResponse.json({ success: true, data: { escrow } });
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('API route error:', error);
|
||||
// return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
// }
|
||||
// }
|
@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { saveTweet, verifySignatureInTweet } from '../../../utils/verifyTweet';
|
||||
import { processTweet, verifySignatureInTweet } from '../../../utils/verifyTweet';
|
||||
import { extractData } from '../../../utils/tweetMessage';
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
@ -21,7 +22,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
}
|
||||
|
||||
const isSigVerified = await verifySignatureInTweet(txSignature);
|
||||
const isHandleCorrect = handle === process.env.NEXT_PUBLIC_ACCOUNT_HANDLE;
|
||||
const isHandleCorrect = handle === process.env.NEXT_PUBLIC_TWITTER_HANDLE;
|
||||
|
||||
// TODO: Verify dynamic page URL in tweet
|
||||
const isVerified = isSigVerified && isHandleCorrect;
|
||||
@ -29,12 +30,10 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
throw new Error('Tweet is not valid');
|
||||
}
|
||||
|
||||
const { isFourthUser } = await saveTweet({ transactionSignature: txSignature, url: memeUrl });
|
||||
if (isFourthUser) {
|
||||
createTokenLockForRecipient();
|
||||
}
|
||||
// Verify and store valid tweet
|
||||
const result = await processTweet(txSignature, memeUrl);
|
||||
|
||||
return NextResponse.json({ success: isVerified, message: 'Tweet verified' })
|
||||
return NextResponse.json(result)
|
||||
} catch (error) {
|
||||
console.error('Error while verifying tweet:', error)
|
||||
return NextResponse.json(
|
||||
@ -43,24 +42,3 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const extractData = (tweet: string | object) => {
|
||||
const tweetText = typeof tweet === 'string' ? tweet : JSON.stringify(tweet);
|
||||
|
||||
const decodedTweet = tweetText.replace(/'/g, "'").replace(/"/g, '"');
|
||||
|
||||
const urlMatch = decodedTweet.match(/<a href="(https:\/\/t.co\/[^"]+)">/);
|
||||
const txSignatureMatch = decodedTweet.match(/TX Hash: '([^']+)'/);
|
||||
const handleMatch = decodedTweet.match(/@([A-Za-z0-9_]+)/);
|
||||
|
||||
return {
|
||||
memeUrl: urlMatch ? urlMatch[1] : null,
|
||||
txSignature: txSignatureMatch ? txSignatureMatch[1].trim() : null,
|
||||
handle: handleMatch ? handleMatch[1] : null,
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Implement function to create lock for a recipient
|
||||
const createTokenLockForRecipient = () => {
|
||||
console.log('Lock created');
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Big from 'big.js';
|
||||
import BN from 'bn.js';
|
||||
|
||||
import WalletHeader from '../components/WalletHeader'
|
||||
@ -61,6 +62,18 @@ const Page: React.FC = (): React.ReactElement => {
|
||||
}
|
||||
}
|
||||
|
||||
const roundUpBigNumber = (bnValue: BN): BN => {
|
||||
const bigNumber = new Big(bnValue.toString());
|
||||
const bigNumberInUnits = bigNumber.div(new Big(10 ** 6));
|
||||
|
||||
const roundedUpValue = bigNumberInUnits.round(0, Big.roundUp);
|
||||
|
||||
// Multiply by 10^6 to revert back to original scale
|
||||
const scaledValue = roundedUpValue.mul(new Big(10 ** 6));
|
||||
|
||||
return new BN(scaledValue.toString());
|
||||
};
|
||||
|
||||
const handleFluxGeneration = (modelId: string, cost: BN) => {
|
||||
return async (prompt: string): Promise<FluxGenerationResult> => {
|
||||
const { connected, publicKey, type } = walletState;
|
||||
@ -75,7 +88,7 @@ const Page: React.FC = (): React.ReactElement => {
|
||||
// Convert cost in USDC to MTM tokens using the price ratio
|
||||
const paymentResult = await processMTMPayment(
|
||||
publicKey,
|
||||
cost,
|
||||
roundUpBigNumber(cost),
|
||||
type
|
||||
)
|
||||
|
||||
|
@ -5,6 +5,8 @@ import BN from 'bn.js';
|
||||
import Big from 'big.js';
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
import { generateTweetText } from '../utils/tweetMessage';
|
||||
|
||||
interface AIServiceCardProps {
|
||||
title: string
|
||||
description: string
|
||||
@ -21,11 +23,11 @@ interface GenerationState {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const baseUnitToDecimalFormat = (value: BN, decimals: number): string => {
|
||||
const baseUnitToWholeNumber = (value: BN, decimals: number): string => {
|
||||
const bigValue = new Big(value.toString());
|
||||
const factor = new Big(10).pow(decimals);
|
||||
|
||||
return bigValue.div(factor).toFixed(decimals);
|
||||
return bigValue.div(factor).round(0, Big.roundUp).toFixed(0);
|
||||
}
|
||||
|
||||
const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
@ -93,11 +95,11 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
const generateTwitterShareUrl = (imageUrl: string, transactionSignature: string): string => {
|
||||
const baseUrl = window.location.href;
|
||||
const cid = imageUrl.split("/image/")[1];
|
||||
const memeUrl = `${baseUrl}memes/${cid}`;
|
||||
const memePageUrl = `${baseUrl}memes/${cid}`;
|
||||
|
||||
const tweetText = `Check out this meme that I generated! \n TX Hash: '${transactionSignature}' \n @${process.env.NEXT_PUBLIC_ACCOUNT_HANDLE} \n`;
|
||||
const tweetText = generateTweetText(transactionSignature, process.env.NEXT_PUBLIC_TWITTER_HANDLE)
|
||||
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(memeUrl)}`;
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(memePageUrl)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -109,7 +111,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: {priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM
|
||||
Cost: {priceMTM ? baseUnitToWholeNumber(priceMTM, 6) : '...'} MTM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -133,7 +135,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 ${priceMTM ? baseUnitToDecimalFormat(priceMTM, 6) : '...'} MTM & Generate`}
|
||||
{generationState.loading ? 'Processing...' : `Pay ${priceMTM ? baseUnitToWholeNumber(priceMTM, 6) : '...'} MTM & Generate`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -10,4 +10,10 @@ export class Tweet {
|
||||
|
||||
@Column({ unique: true })
|
||||
transactionSignature!: string;
|
||||
|
||||
@Column({ type: 'boolean', nullable: true })
|
||||
isLockCreated!: boolean | null;
|
||||
|
||||
@Column({ type: 'text', unique: true, nullable: true })
|
||||
lockEscrow!: string | null;
|
||||
}
|
||||
|
196
src/locker-utils/index.ts
Normal file
196
src/locker-utils/index.ts
Normal file
@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Methods from jup-lock:
|
||||
* - createLockerProgram
|
||||
* - deriveEscrow
|
||||
* - createVestingPlanV2
|
||||
* Reference: https://github.com/jup-ag/jup-lock/blob/main/tests/locker_utils/index.ts
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import 'dotenv/config';
|
||||
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
getAssociatedTokenAddressSync,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
} from '@solana/spl-token';
|
||||
import {
|
||||
AnchorProvider,
|
||||
BN,
|
||||
Program,
|
||||
Wallet,
|
||||
web3,
|
||||
workspace,
|
||||
} from '@coral-xyz/anchor';
|
||||
import { AccountMeta, Connection, TransactionExpiredTimeoutError } from '@solana/web3.js';
|
||||
|
||||
// TODO: Generate type file from IDL json
|
||||
import { Locker } from '../../target/types/locker';
|
||||
import { TokenExtensionUtil } from './token-2022/token-extensions';
|
||||
import {
|
||||
OptionRemainingAccountsInfoData,
|
||||
RemainingAccountsBuilder,
|
||||
RemainingAccountsType,
|
||||
} from './token-2022/remaining-accounts';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL);
|
||||
|
||||
const connection = new Connection(process.env.NEXT_PUBLIC_SOLANA_RPC_URL);
|
||||
|
||||
export function createLockerProgram(wallet: Wallet): Program<Locker> {
|
||||
const provider = new AnchorProvider(connection, wallet, {
|
||||
maxRetries: 3,
|
||||
});
|
||||
provider.opts.commitment = 'confirmed';
|
||||
|
||||
return workspace.Locker as Program<Locker>;
|
||||
}
|
||||
|
||||
export function deriveEscrow(base: web3.PublicKey, programId: web3.PublicKey) {
|
||||
return web3.PublicKey.findProgramAddressSync(
|
||||
[Buffer.from('escrow'), base.toBuffer()],
|
||||
programId
|
||||
);
|
||||
}
|
||||
|
||||
export interface CreateVestingPlanParams {
|
||||
ownerKeypair: web3.Keypair;
|
||||
tokenMint: web3.PublicKey;
|
||||
isAssertion: boolean;
|
||||
vestingStartTime: BN;
|
||||
cliffTime: BN;
|
||||
frequency: BN;
|
||||
cliffUnlockAmount: BN;
|
||||
amountPerPeriod: BN;
|
||||
numberOfPeriod: BN;
|
||||
recipient: web3.PublicKey;
|
||||
updateRecipientMode: number;
|
||||
cancelMode: number;
|
||||
tokenProgram?: web3.PublicKey;
|
||||
}
|
||||
|
||||
// V2 instructions
|
||||
export async function createVestingPlanV2(params: CreateVestingPlanParams) {
|
||||
let {
|
||||
ownerKeypair,
|
||||
tokenMint,
|
||||
isAssertion,
|
||||
vestingStartTime,
|
||||
cliffTime,
|
||||
frequency,
|
||||
cliffUnlockAmount,
|
||||
amountPerPeriod,
|
||||
numberOfPeriod,
|
||||
recipient,
|
||||
updateRecipientMode,
|
||||
cancelMode,
|
||||
tokenProgram,
|
||||
} = params;
|
||||
|
||||
const program = createLockerProgram(new Wallet(ownerKeypair));
|
||||
|
||||
const baseKP = web3.Keypair.generate();
|
||||
|
||||
let [escrow] = deriveEscrow(baseKP.publicKey, program.programId);
|
||||
|
||||
const senderToken = getAssociatedTokenAddressSync(
|
||||
tokenMint,
|
||||
ownerKeypair.publicKey,
|
||||
false,
|
||||
tokenProgram,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
);
|
||||
|
||||
const escrowToken = getAssociatedTokenAddressSync(
|
||||
tokenMint,
|
||||
escrow,
|
||||
true,
|
||||
tokenProgram,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
);
|
||||
|
||||
let remainingAccountsInfo: OptionRemainingAccountsInfoData | null = null;
|
||||
let remainingAccounts: AccountMeta[] = [];
|
||||
if (tokenProgram == TOKEN_2022_PROGRAM_ID) {
|
||||
let inputTransferHookAccounts =
|
||||
await TokenExtensionUtil.getExtraAccountMetasForTransferHook(
|
||||
program.provider.connection,
|
||||
tokenMint,
|
||||
senderToken,
|
||||
escrowToken,
|
||||
ownerKeypair.publicKey,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
);
|
||||
|
||||
[remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder()
|
||||
.addSlice(
|
||||
RemainingAccountsType.TransferHookEscrow,
|
||||
inputTransferHookAccounts
|
||||
)
|
||||
.build() as [OptionRemainingAccountsInfoData, AccountMeta[]];
|
||||
}
|
||||
|
||||
assert(tokenProgram);
|
||||
|
||||
try {
|
||||
await program.methods
|
||||
.createVestingEscrowV2(
|
||||
{
|
||||
vestingStartTime,
|
||||
cliffTime,
|
||||
frequency,
|
||||
cliffUnlockAmount,
|
||||
amountPerPeriod,
|
||||
numberOfPeriod,
|
||||
updateRecipientMode,
|
||||
cancelMode,
|
||||
},
|
||||
remainingAccountsInfo
|
||||
)
|
||||
.accounts({
|
||||
base: baseKP.publicKey,
|
||||
senderToken,
|
||||
escrowToken,
|
||||
recipient,
|
||||
tokenMint,
|
||||
sender: ownerKeypair.publicKey,
|
||||
tokenProgram,
|
||||
systemProgram: web3.SystemProgram.programId,
|
||||
escrow,
|
||||
// TODO: Fix type error for escrowToken
|
||||
} as any)
|
||||
.remainingAccounts(remainingAccounts ? remainingAccounts : [])
|
||||
.preInstructions([
|
||||
createAssociatedTokenAccountInstruction(
|
||||
ownerKeypair.publicKey,
|
||||
escrowToken,
|
||||
escrow,
|
||||
tokenMint,
|
||||
tokenProgram,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
),
|
||||
])
|
||||
.signers([baseKP, ownerKeypair])
|
||||
.rpc();
|
||||
|
||||
return escrow;
|
||||
} catch (error) {
|
||||
if (error instanceof TransactionExpiredTimeoutError) {
|
||||
console.error('Transaction confirmation delayed for', error.signature);
|
||||
console.log('Confirming the transaction again...');
|
||||
const confirmedTransaction = await connection.getTransaction(error.signature, {
|
||||
commitment: 'confirmed',
|
||||
maxSupportedTransactionVersion: 0
|
||||
});
|
||||
|
||||
if(confirmedTransaction === null) {
|
||||
console.error('Transaction failed for', error.signature);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return escrow;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
50
src/locker-utils/token-2022/remaining-accounts.ts
Normal file
50
src/locker-utils/token-2022/remaining-accounts.ts
Normal file
@ -0,0 +1,50 @@
|
||||
// Reference: https://github.com/jup-ag/jup-lock/blob/main/tests/locker_utils/index.ts
|
||||
|
||||
import { AccountMeta } from '@solana/web3.js';
|
||||
|
||||
export enum RemainingAccountsType {
|
||||
TransferHookEscrow = 'transferHookEscrow',
|
||||
}
|
||||
|
||||
type RemainingAccountsAnchorType = { transferHookEscrow: {} };
|
||||
|
||||
export type RemainingAccountsSliceData = {
|
||||
accountsType: RemainingAccountsAnchorType;
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type RemainingAccountsInfoData = {
|
||||
slices: RemainingAccountsSliceData[];
|
||||
};
|
||||
|
||||
// Option<RemainingAccountsInfoData> on Rust
|
||||
// null is treated as None in Rust. undefined doesn't work.
|
||||
export type OptionRemainingAccountsInfoData = RemainingAccountsInfoData | null;
|
||||
|
||||
export class RemainingAccountsBuilder {
|
||||
private remainingAccounts: AccountMeta[] = [];
|
||||
private slices: RemainingAccountsSliceData[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
addSlice(
|
||||
accountsType: RemainingAccountsType,
|
||||
accounts?: AccountMeta[]
|
||||
): this {
|
||||
if (!accounts || accounts.length === 0) return this;
|
||||
|
||||
this.slices.push({
|
||||
accountsType: { [accountsType]: {} } as RemainingAccountsAnchorType,
|
||||
length: accounts.length,
|
||||
});
|
||||
this.remainingAccounts.push(...accounts);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): [OptionRemainingAccountsInfoData, AccountMeta[] | undefined] {
|
||||
return this.slices.length === 0
|
||||
? [null, undefined]
|
||||
: [{ slices: this.slices }, this.remainingAccounts];
|
||||
}
|
||||
}
|
57
src/locker-utils/token-2022/token-extensions.ts
Normal file
57
src/locker-utils/token-2022/token-extensions.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// Reference: https://github.com/jup-ag/jup-lock/blob/main/tests/locker_utils/index.ts
|
||||
|
||||
import {
|
||||
AccountMeta,
|
||||
Connection,
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import {
|
||||
addExtraAccountsToInstruction,
|
||||
getMint,
|
||||
getTransferHook,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
} from '@solana/spl-token';
|
||||
|
||||
export class TokenExtensionUtil {
|
||||
public static async getExtraAccountMetasForTransferHook(
|
||||
connection: Connection,
|
||||
tokenMint: PublicKey,
|
||||
source: PublicKey,
|
||||
destination: PublicKey,
|
||||
owner: PublicKey,
|
||||
tokenProgram: PublicKey
|
||||
): Promise<AccountMeta[] | undefined> {
|
||||
let mint = await getMint(connection, tokenMint, 'confirmed', tokenProgram);
|
||||
const transferHook = getTransferHook(mint);
|
||||
|
||||
if (!transferHook) return undefined;
|
||||
|
||||
const instruction = new TransactionInstruction({
|
||||
programId: TOKEN_2022_PROGRAM_ID,
|
||||
keys: [
|
||||
{ pubkey: source, isSigner: false, isWritable: false },
|
||||
{
|
||||
pubkey: tokenMint,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{ pubkey: destination, isSigner: false, isWritable: false },
|
||||
{ pubkey: owner, isSigner: false, isWritable: false },
|
||||
{ pubkey: owner, isSigner: false, isWritable: false },
|
||||
],
|
||||
});
|
||||
|
||||
// Note:
|
||||
await addExtraAccountsToInstruction(
|
||||
connection,
|
||||
instruction,
|
||||
tokenMint,
|
||||
'confirmed',
|
||||
transferHook.programId,
|
||||
);
|
||||
|
||||
const extraAccountMetas = instruction.keys.slice(5);
|
||||
return extraAccountMetas.length > 0 ? extraAccountMetas : undefined;
|
||||
}
|
||||
}
|
@ -12,11 +12,11 @@ import {
|
||||
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_MTM_MINT_ADDRESS, 'MTM_MINT_ADDRESS is required');
|
||||
assert(process.env.NEXT_PUBLIC_MTM_RECIPIENT_MULTISIG_ADDRESS, 'MTM_RECIPIENT_MULTISIG_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 MTM_TOKEN_MINT = process.env.NEXT_PUBLIC_MTM_MINT_ADDRESS;
|
||||
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_MTM_RECIPIENT_MULTISIG_ADDRESS;
|
||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL;
|
||||
|
||||
|
106
src/utils/create-lock.ts
Normal file
106
src/utils/create-lock.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import assert from 'assert';
|
||||
import BN from 'bn.js';
|
||||
import 'dotenv/config';
|
||||
import bs58 from 'bs58';
|
||||
import Big from 'big.js';
|
||||
|
||||
import * as anchor from "@coral-xyz/anchor";
|
||||
import {
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
||||
|
||||
import { createVestingPlanV2 } from '../locker-utils';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL);
|
||||
assert(process.env.WSOL_LOCKER_ACCOUNT_PK);
|
||||
assert(process.env.WSOL_MINT_ADDRESS);
|
||||
|
||||
const RPC_ENDPOINT= process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
const WSOL_MINT_ADDRESS = process.env.WSOL_MINT_ADDRESS;
|
||||
const WSOL_LOCKER_ACCOUNT_PK = process.env.WSOL_LOCKER_ACCOUNT_PK;
|
||||
|
||||
const userKP = anchor.web3.Keypair.fromSecretKey(bs58.decode(WSOL_LOCKER_ACCOUNT_PK));
|
||||
|
||||
const connection = new Connection(RPC_ENDPOINT);
|
||||
const token = new PublicKey(WSOL_MINT_ADDRESS);
|
||||
|
||||
const provider = new anchor.AnchorProvider(
|
||||
connection,
|
||||
new anchor.Wallet(userKP),
|
||||
// Commitment level required for simulating transaction
|
||||
{ preflightCommitment: 'processed' }
|
||||
);
|
||||
|
||||
anchor.setProvider(provider);
|
||||
|
||||
export async function createLock(tokenLockerKeypair: anchor.web3.Keypair, recipientPubKey: anchor.web3.PublicKey, duration: BN, balance: BN): Promise<anchor.web3.PublicKey | undefined> {
|
||||
|
||||
if (balance.eq(new BN(0))) {
|
||||
console.log('No balance available to create lock, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Creating a lock...');
|
||||
|
||||
const escrow = await createVestingPlanV2({
|
||||
ownerKeypair: tokenLockerKeypair,
|
||||
vestingStartTime: new BN(Math.floor(Date.now() / 1000) - 60), // Start immediately
|
||||
tokenMint: token,
|
||||
isAssertion: true,
|
||||
cliffTime: duration,
|
||||
frequency: new BN(1), // Not needed since full unlock happens at cliff
|
||||
cliffUnlockAmount: balance, // The entire amount should be released at cliff
|
||||
amountPerPeriod: new BN(0), // No tokens should be released before cliff
|
||||
numberOfPeriod: new BN(1), // Only release tokens once
|
||||
recipient: recipientPubKey,
|
||||
updateRecipientMode: 0,
|
||||
cancelMode: 1, // Only creator can cancel the lock
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
});
|
||||
|
||||
if (escrow) {
|
||||
console.log('Lock created successfully:',escrow.toString());
|
||||
}
|
||||
|
||||
return escrow;
|
||||
}
|
||||
|
||||
export async function extractInfo(transactionSignature: string) {
|
||||
const transaction = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
if (!transaction) {
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
|
||||
const transferInstruction = transaction.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||
);
|
||||
|
||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||
throw new Error('Transfer instruction not found');
|
||||
}
|
||||
|
||||
const { info: { amount, authority } } = transferInstruction.parsed;
|
||||
return { authority, amount };
|
||||
}
|
||||
|
||||
export async function createRewardLock(authority: string, amount: string) {
|
||||
const { WSOL_LOCKER_ACCOUNT_PK, WSOL_LOCK_DURATION_IN_SECONDS, WSOL_MINT_ADDRESS, NEXT_PUBLIC_MTM_MINT_ADDRESS, REWARD_MULTIPLIER } = process.env;
|
||||
if (!WSOL_LOCKER_ACCOUNT_PK || !WSOL_LOCK_DURATION_IN_SECONDS || !WSOL_MINT_ADDRESS || !NEXT_PUBLIC_MTM_MINT_ADDRESS || !REWARD_MULTIPLIER) {
|
||||
throw new Error('Missing required environment variables');
|
||||
}
|
||||
|
||||
const duration = new BN(WSOL_LOCK_DURATION_IN_SECONDS).add(new BN(Math.floor(Date.now() / 1000)));
|
||||
const tokenLockerKeypair = Keypair.fromSecretKey(bs58.decode(WSOL_LOCKER_ACCOUNT_PK));
|
||||
const recipientPublicKey = new PublicKey(authority);
|
||||
|
||||
const url = `https://api.jup.ag/price/v2?ids=${NEXT_PUBLIC_MTM_MINT_ADDRESS}&vsToken=${WSOL_MINT_ADDRESS}`;
|
||||
const response = await fetch(url);
|
||||
const { data } = await response.json();
|
||||
|
||||
const priceWSOLFor1MTM = new Big(data[NEXT_PUBLIC_MTM_MINT_ADDRESS].price).toFixed(9);
|
||||
const mtmAmount = new Big(amount).div(new Big(10).pow(6));
|
||||
const wsolAmount = new BN(new Big(mtmAmount).times(priceWSOLFor1MTM).times(new Big(10).pow(9)).times(REWARD_MULTIPLIER).toFixed(0));
|
||||
|
||||
return createLock(tokenLockerKeypair, recipientPublicKey, duration, wsolAmount);
|
||||
}
|
22
src/utils/extractTxInfo.ts
Normal file
22
src/utils/extractTxInfo.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
|
||||
const connection = new Connection(process.env.NEXT_PUBLIC_SOLANA_RPC_URL!);
|
||||
|
||||
export async function extractTxInfo(transactionSignature: string) {
|
||||
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
if (!result) {
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
|
||||
const transferInstruction = result.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||
);
|
||||
|
||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||
throw new Error('Transfer instruction not found');
|
||||
}
|
||||
|
||||
const { info: { amount, authority } } = transferInstruction.parsed;
|
||||
return { authority, amount };
|
||||
}
|
19
src/utils/tweetMessage.ts
Normal file
19
src/utils/tweetMessage.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export const generateTweetText = (transactionSignature: string, handle: string | undefined) => {
|
||||
return `Check out this meme that I generated! \n TX Hash: '${transactionSignature}' \n @${handle} \n`;
|
||||
};
|
||||
|
||||
export const extractData = (tweet: string | object) => {
|
||||
const tweetText = typeof tweet === 'string' ? tweet : JSON.stringify(tweet);
|
||||
|
||||
const decodedTweet = tweetText.replace(/'/g, "'").replace(/"/g, '"');
|
||||
|
||||
const urlMatch = decodedTweet.match(/<a href="(https:\/\/t.co\/[^"]+)">/);
|
||||
const txSignatureMatch = decodedTweet.match(/TX Hash: '([^']+)'/);
|
||||
const handleMatch = decodedTweet.match(/@([A-Za-z0-9_]+)/);
|
||||
|
||||
return {
|
||||
memeUrl: urlMatch ? urlMatch[1] : null,
|
||||
txSignature: txSignatureMatch ? txSignatureMatch[1].trim() : null,
|
||||
handle: handleMatch ? handleMatch[1] : null,
|
||||
};
|
||||
};
|
@ -3,8 +3,7 @@ import BN from 'bn.js';
|
||||
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
|
||||
import { Payment } from '../entity/Payment';
|
||||
import { extractTxInfo } from './extractTxInfo';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
|
||||
@ -44,6 +43,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Verify that payment receiver is correct
|
||||
export async function verifyPayment(
|
||||
transactionSignature: string,
|
||||
tokenAmount: BN,
|
||||
@ -54,22 +54,7 @@ export async function verifyPayment(
|
||||
return false;
|
||||
}
|
||||
|
||||
const transaction = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
if (!transaction) {
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
|
||||
const transferInstruction = transaction.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||
);
|
||||
|
||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||
throw new Error('Transfer instruction not found');
|
||||
}
|
||||
|
||||
const { parsed } = transferInstruction;
|
||||
const { info } = parsed;
|
||||
const { amount } = info;
|
||||
const { amount } = await extractTxInfo(transactionSignature);
|
||||
|
||||
const transactionAmount = new BN(amount);
|
||||
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { DataSource, EntityTarget } from 'typeorm';
|
||||
|
||||
import { Tweet } from '../entity/Tweet';
|
||||
import { createRewardLock } from './create-lock';
|
||||
import { extractTxInfo } from './extractTxInfo';
|
||||
|
||||
export async function verifySignatureInTweet(transactionSignature: string): Promise<boolean> {
|
||||
const paymentRepository = global.appDataSource.getRepository(global.entities.Payment);
|
||||
@ -18,14 +22,48 @@ export async function verifySignatureInTweet(transactionSignature: string): Prom
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function saveTweet(data: Partial<Tweet>): Promise<{ isFourthUser: boolean }> {
|
||||
return await global.appDataSource.transaction(async (transactionalEntityManager) => {
|
||||
const tweetRepository = transactionalEntityManager.getRepository(global.entities.Tweet);
|
||||
export async function processTweet(txSignature: string, memeUrl: string | null) {
|
||||
const tweetRepository = (global.appDataSource as DataSource).getRepository(
|
||||
global.entities.Tweet as EntityTarget<Tweet>
|
||||
);
|
||||
|
||||
const tweet = await tweetRepository.save(data);
|
||||
|
||||
return {
|
||||
isFourthUser: tweet.id % 4 === 0
|
||||
};
|
||||
const tweet = await tweetRepository.save({
|
||||
transactionSignature: txSignature,
|
||||
url: memeUrl,
|
||||
});
|
||||
|
||||
const isFourthUser = tweet.id % 4 === 0;
|
||||
|
||||
try {
|
||||
if (isFourthUser) {
|
||||
const { authority, amount } = await extractTxInfo(txSignature);
|
||||
|
||||
if (!authority || Number(amount) <= 0) {
|
||||
return { error: "Invalid transaction details" };
|
||||
}
|
||||
|
||||
const escrow = await createRewardLock(authority, amount);
|
||||
|
||||
if (!escrow) {
|
||||
throw new Error("Lock not created");
|
||||
}
|
||||
|
||||
await tweetRepository.update(tweet.id, {
|
||||
isLockCreated: true,
|
||||
lockEscrow: escrow.toString()
|
||||
});
|
||||
|
||||
return { success: true, data: { escrow } };
|
||||
}
|
||||
|
||||
return { success: true, message: 'Tweet verified' };
|
||||
} catch (error) {
|
||||
await tweetRepository.update(tweet.id, {
|
||||
isLockCreated: false,
|
||||
});
|
||||
|
||||
console.error('Error locking tokens: ', error);
|
||||
|
||||
throw new Error("Transaction failed.");
|
||||
}
|
||||
}
|
||||
|
3143
target/idl/locker.json
Normal file
3143
target/idl/locker.json
Normal file
File diff suppressed because it is too large
Load Diff
3149
target/types/locker.ts
Normal file
3149
target/types/locker.ts
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user