forked from mito-systems/sol-mem-gen
Compare commits
No commits in common. "sk-db-path-env" and "main" have entirely different histories.
sk-db-path
...
main
30
.env.example
30
.env.example
@ -1,30 +0,0 @@
|
|||||||
# Get key from https://fal.ai
|
|
||||||
FAL_AI_KEY=
|
|
||||||
|
|
||||||
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=
|
|
||||||
|
|
||||||
# Get the gateway from https://app.pinata.cloud/gateway
|
|
||||||
PINATA_GATEWAY=
|
|
||||||
|
|
||||||
# Change to your website URL
|
|
||||||
# For development: SITE_URL=http://localhost:3000
|
|
||||||
SITE_URL=https://memes.markto.market
|
|
||||||
VERIFY_TWEET_SECRET=
|
|
||||||
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
|
|
||||||
MAX_RETRIES_FOR_LOCK_TX=5
|
|
||||||
|
|
||||||
DB_PATH='./database.sqlite'
|
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.next
|
|
||||||
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development
|
|
||||||
.env.production
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
database.sqlite
|
|
||||||
dist
|
|
@ -1,2 +0,0 @@
|
|||||||
[provider]
|
|
||||||
cluster = "mainnet"
|
|
79
README.md
79
README.md
@ -1,79 +0,0 @@
|
|||||||
# sol-mem-gen
|
|
||||||
|
|
||||||
This project is a Solana-based meme generator that allows users to connect their wallets, pay with MTM tokens, and generate memes using AI models.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Connect to Solflare and Phantom wallets
|
|
||||||
- Pay with MTM tokens
|
|
||||||
- Generate memes using various AI models
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Node.js
|
|
||||||
- npm
|
|
||||||
- Solflare or Phantom wallet
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
- Install dependencies:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
- Copy the `.env.example` file to `.env` and add your API keys:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cp .env.example .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
- Add your FAL AI key to the `.env` file:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Get key from https://fal.ai/
|
|
||||||
FAL_AI_KEY=your_fal_ai_key
|
|
||||||
```
|
|
||||||
|
|
||||||
- Setup a project on <https://app.pinata.cloud>
|
|
||||||
|
|
||||||
- Visit <https://app.pinata.cloud/developers/api-keys> and click the `New Key` button to create a new key
|
|
||||||
|
|
||||||
- Choose the `Admin` scope and create the API key
|
|
||||||
|
|
||||||
- Copy the JWT that is displayed
|
|
||||||
|
|
||||||
- Add the following to `.env`:
|
|
||||||
|
|
||||||
```env
|
|
||||||
PINATA_JWT=
|
|
||||||
|
|
||||||
# Get the gateway from https://app.pinata.cloud/gateway
|
|
||||||
PINATA_GATEWAY=
|
|
||||||
|
|
||||||
# Mark to market twitter handle: @mark_2_market0
|
|
||||||
NEXT_PUBLIC_TWITTER_HANDLE=
|
|
||||||
|
|
||||||
# Add a secret random string that will be used to access the page to submit tweets for verification
|
|
||||||
# This secret will be checked against the URL to either allow or deny access
|
|
||||||
# The URL should look something like this: http://localhost:3000/submit-tweets/<your-verify-tweet-secret>
|
|
||||||
VERIFY_TWEET_SECRET=
|
|
||||||
```
|
|
||||||
|
|
||||||
- Run the development server:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Open your browser and navigate to `http://localhost:3000`.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- Connect your Solflare or Phantom wallet using the "Connect Wallet" button.
|
|
||||||
- Select an AI model and enter a prompt to generate a meme.
|
|
||||||
- Pay the required MTM tokens and wait for the meme to be generated.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
Follow [instructions](./deploy/README.md) for deploying `sol-mem-gen` and `sol-token-locker`
|
|
@ -1,10 +0,0 @@
|
|||||||
# ENV for registry operations
|
|
||||||
|
|
||||||
# Bond to use
|
|
||||||
REGISTRY_BOND_ID=
|
|
||||||
|
|
||||||
# Target deployer LRN
|
|
||||||
DEPLOYER_LRN=
|
|
||||||
|
|
||||||
# Authority to deploy the app under
|
|
||||||
AUTHORITY=
|
|
@ -1,40 +0,0 @@
|
|||||||
ARG VARIANT=20-bullseye
|
|
||||||
FROM node:${VARIANT}
|
|
||||||
|
|
||||||
ARG USERNAME=node
|
|
||||||
ARG NPM_GLOBAL=/usr/local/share/npm-global
|
|
||||||
|
|
||||||
# Add NPM global to PATH.
|
|
||||||
ENV PATH=${NPM_GLOBAL}/bin:${PATH}
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
# Configure global npm install location, use group to adapt to UID/GID changes
|
|
||||||
if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \
|
|
||||||
&& usermod -a -G npm ${USERNAME} \
|
|
||||||
&& umask 0002 \
|
|
||||||
&& mkdir -p ${NPM_GLOBAL} \
|
|
||||||
&& touch /usr/local/etc/npmrc \
|
|
||||||
&& chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \
|
|
||||||
&& chmod g+s ${NPM_GLOBAL} \
|
|
||||||
&& npm config -g set prefix ${NPM_GLOBAL} \
|
|
||||||
&& su ${USERNAME} -c "npm config -g set prefix ${NPM_GLOBAL}" \
|
|
||||||
# Install eslint
|
|
||||||
&& su ${USERNAME} -c "umask 0002 && npm install -g eslint" \
|
|
||||||
&& npm cache clean --force > /dev/null 2>&1
|
|
||||||
|
|
||||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get -y install --no-install-recommends jq bash
|
|
||||||
|
|
||||||
# laconic-so
|
|
||||||
RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/latest/laconic-so && \
|
|
||||||
chmod +x ./laconic-so && \
|
|
||||||
mv ./laconic-so /usr/bin/laconic-so
|
|
||||||
|
|
||||||
# Configure the npm registry
|
|
||||||
RUN npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
|
|
||||||
|
|
||||||
# DEBUG, remove
|
|
||||||
RUN yarn info @cerc-io/laconic-registry-cli
|
|
||||||
|
|
||||||
# Globally install the cli package
|
|
||||||
RUN yarn global add @cerc-io/laconic-registry-cli
|
|
121
deploy/README.md
121
deploy/README.md
@ -1,121 +0,0 @@
|
|||||||
# Deploy
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
### sol-mem-gen
|
|
||||||
|
|
||||||
* Clone the repo:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone git@git.vdb.to:deep-stack/sol-mem-gen.git
|
|
||||||
cd sol-mem-gen/deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
* Build registry CLI image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t cerc/laconic-registry-cli .
|
|
||||||
|
|
||||||
# Builds image cerc/laconic-registry-cli:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
* Configure `userKey` and `bondId` in the registry CLI config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano config.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
* Add configuration for registry operations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .registry.env.example .registry.env
|
|
||||||
|
|
||||||
# Fill in the required values
|
|
||||||
nano .registry.env
|
|
||||||
```
|
|
||||||
|
|
||||||
* Add configuration for the app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s https://git.vdb.to/deep-stack/sol-mem-gen/raw/branch/main/.env.example -o .app.env
|
|
||||||
|
|
||||||
# Fill in the required values
|
|
||||||
nano .app.env
|
|
||||||
```
|
|
||||||
|
|
||||||
### sol-token-locker
|
|
||||||
|
|
||||||
* Create a dir for `sol-token-locker` deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In deployments dir
|
|
||||||
mkdir sol-token-locker
|
|
||||||
cd sol-token-locker
|
|
||||||
```
|
|
||||||
|
|
||||||
* Follow [instructions](https://git.vdb.to/deep-stack/sol-token-locker/src/branch/main/stack-orchestrator/stacks/sol-token-locker/README.md) to setup the `sol-token-locker` stack in `sol-token-locker` directory
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
### sol-mem-gen
|
|
||||||
|
|
||||||
* Update configuration for the app as required:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In sol-mem-gen/deploy dir
|
|
||||||
nano sol-mem-gen/.app.env
|
|
||||||
```
|
|
||||||
|
|
||||||
* Deploy `sol-mem-gen` App:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In sol-mem-gen/deploy dir
|
|
||||||
docker run -it \
|
|
||||||
-v ./:/app/deploy -w /app/deploy \
|
|
||||||
-e DEPLOYMENT_DNS=memes.markto.market \
|
|
||||||
cerc/laconic-registry-cli:latest \
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
* Check deployment logs on deployer UI: <https://webapp-deployer-ui.apps.vaasl.io/>
|
|
||||||
|
|
||||||
* Visit deployed app: <https://memes.markto.market>
|
|
||||||
|
|
||||||
* To redeploy the app (using <https://git.vdb.to/deep-stack/sol-mem-gen> `main` branch), repeat the previous steps
|
|
||||||
|
|
||||||
### sol-token-locker
|
|
||||||
|
|
||||||
* Update configuration for token locker as required:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In deployments dir
|
|
||||||
cd sol-token-locker
|
|
||||||
nano sol-token-locker-deployment/config.env
|
|
||||||
```
|
|
||||||
|
|
||||||
* [Config reference](https://git.vdb.to/deep-stack/sol-token-locker/src/branch/main/stack-orchestrator/stacks/sol-token-locker/README.md#configuration)
|
|
||||||
|
|
||||||
* Start the deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
laconic-so deployment --dir sol-token-locker-deployment start
|
|
||||||
```
|
|
||||||
|
|
||||||
* Follow logs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
laconic-so deployment --dir sol-token-locker-deployment logs sol-token-locker -f
|
|
||||||
```
|
|
||||||
|
|
||||||
* For updating the deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop the deployment
|
|
||||||
laconic-so deployment --dir sol-token-locker-deployment stop
|
|
||||||
|
|
||||||
# Update the config as required
|
|
||||||
nano sol-token-locker-deployment/config.env
|
|
||||||
|
|
||||||
# Re-start deployment
|
|
||||||
laconic-so deployment --dir sol-token-locker-deployment start
|
|
||||||
```
|
|
@ -1,10 +0,0 @@
|
|||||||
# Registry CLI config
|
|
||||||
services:
|
|
||||||
registry:
|
|
||||||
rpcEndpoint: 'https://laconicd-sapo.laconic.com'
|
|
||||||
gqlEndpoint: 'https://laconicd-sapo.laconic.com/api'
|
|
||||||
userKey:
|
|
||||||
bondId:
|
|
||||||
chainId: laconic-testnet-2
|
|
||||||
fees: 2.5
|
|
||||||
gasPrice: 0.01alnt
|
|
126
deploy/deploy.sh
126
deploy/deploy.sh
@ -1,126 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Fail on error
|
|
||||||
set -e
|
|
||||||
|
|
||||||
source .registry.env
|
|
||||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
|
||||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
|
||||||
echo "Using AUTHORITY: $AUTHORITY"
|
|
||||||
|
|
||||||
# Repository URL
|
|
||||||
REPO_URL="https://git.vdb.to/deep-stack/sol-mem-gen"
|
|
||||||
|
|
||||||
# Get the latest commit hash from the repository
|
|
||||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
|
||||||
|
|
||||||
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/main/package.json | jq -r .version)
|
|
||||||
APP_NAME=sol-mem-gen
|
|
||||||
|
|
||||||
echo "Repo: ${REPO_URL}"
|
|
||||||
echo "Latest hash: ${LATEST_HASH}"
|
|
||||||
echo "App version: ${PACKAGE_VERSION}"
|
|
||||||
echo "Deployment DNS: ${DEPLOYMENT_DNS}"
|
|
||||||
|
|
||||||
# Current date and time for note
|
|
||||||
CURRENT_DATE_TIME=$(date -u)
|
|
||||||
|
|
||||||
CONFIG_FILE=config.yml
|
|
||||||
|
|
||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
|
||||||
NEW_APPLICATION_VERSION=$(laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
|
||||||
|
|
||||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
|
||||||
# Set application-record version if no previous records were found
|
|
||||||
NEW_APPLICATION_VERSION=0.0.1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
|
||||||
mkdir -p records
|
|
||||||
RECORD_FILE=./records/application-record.yml
|
|
||||||
|
|
||||||
cat >$RECORD_FILE <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: $NEW_APPLICATION_VERSION
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
repository: ["$REPO_URL"]
|
|
||||||
app_type: webapp
|
|
||||||
name: $APP_NAME
|
|
||||||
app_version: $PACKAGE_VERSION
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Application record generated successfully: $RECORD_FILE"
|
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
|
||||||
publish_response=$(laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
|
||||||
rc=$?
|
|
||||||
if [ $rc -ne 0 ]; then
|
|
||||||
echo "FATAL: Failed to publish record"
|
|
||||||
exit $rc
|
|
||||||
fi
|
|
||||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
|
||||||
echo "ApplicationRecord published, setting names next"
|
|
||||||
echo $RECORD_ID
|
|
||||||
|
|
||||||
# Set name to record
|
|
||||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/$APP_NAME"
|
|
||||||
|
|
||||||
name1="$REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
|
||||||
sleep 2
|
|
||||||
laconic -c $CONFIG_FILE registry name set "$name1" "$RECORD_ID"
|
|
||||||
rc=$?
|
|
||||||
if [ $rc -ne 0 ]; then
|
|
||||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
|
||||||
exit $rc
|
|
||||||
fi
|
|
||||||
echo "$name1 set for ApplicationRecord"
|
|
||||||
|
|
||||||
name2="$REGISTRY_APP_LRN@${LATEST_HASH}"
|
|
||||||
sleep 2
|
|
||||||
laconic -c $CONFIG_FILE registry name set "$name2" "$RECORD_ID"
|
|
||||||
rc=$?
|
|
||||||
if [ $rc -ne 0 ]; then
|
|
||||||
echo "FATAL: Failed to set hash"
|
|
||||||
exit $rc
|
|
||||||
fi
|
|
||||||
echo "$name2 set for ApplicationRecord"
|
|
||||||
|
|
||||||
name3="$REGISTRY_APP_LRN"
|
|
||||||
sleep 2
|
|
||||||
# Set name if latest release
|
|
||||||
laconic -c $CONFIG_FILE registry name set "$name3" "$RECORD_ID"
|
|
||||||
rc=$?
|
|
||||||
if [ $rc -ne 0 ]; then
|
|
||||||
echo "FATAL: Failed to set release"
|
|
||||||
exit $rc
|
|
||||||
fi
|
|
||||||
echo "$name3 set for ApplicationRecord"
|
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_LRN
|
|
||||||
query_response=$(laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
|
||||||
rc=$?
|
|
||||||
if [ $rc -ne 0 ]; then
|
|
||||||
echo "FATAL: Failed to query name"
|
|
||||||
exit $rc
|
|
||||||
fi
|
|
||||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
|
||||||
echo "No record found for $REGISTRY_APP_LRN."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Name resolution successful"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
echo "Requesting a webapp deployment for $name2, using deployer $DEPLOYER_LRN"
|
|
||||||
laconic-so request-webapp-deployment \
|
|
||||||
--laconic-config $CONFIG_FILE \
|
|
||||||
--deployer $DEPLOYER_LRN \
|
|
||||||
--app $name2 \
|
|
||||||
--env-file ./.app.env \
|
|
||||||
--dns $DEPLOYMENT_DNS \
|
|
||||||
--make-payment auto
|
|
||||||
|
|
||||||
echo "Done"
|
|
@ -1,8 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
3380
package-lock.json
generated
3380
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -3,32 +3,22 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node --project tsconfig.server.json server.ts",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "tsc --project tsconfig.server.json && NODE_ENV=production PORT=80 node dist/server.js",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coral-xyz/anchor": "^0.30.1",
|
|
||||||
"@fal-ai/client": "^1.2.1",
|
"@fal-ai/client": "^1.2.1",
|
||||||
"@google/generative-ai": "^0.21.0",
|
"@google/generative-ai": "^0.21.0",
|
||||||
"@solana/spl-token": "^0.3.11",
|
"@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",
|
||||||
"pinata-web3": "^0.5.4",
|
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18"
|
||||||
"sqlite3": "^5.1.7",
|
|
||||||
"typeorm": "^0.3.12"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/big.js": "^6.2.2",
|
|
||||||
"@types/bn.js": "^5.1.6",
|
|
||||||
"@types/bs58": "^4.0.4",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
@ -37,7 +27,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
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_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_MINT_ADDRESS;
|
|
||||||
const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT_ADDRESS;
|
|
||||||
|
|
||||||
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 };
|
|
56
server.ts
56
server.ts
@ -1,56 +0,0 @@
|
|||||||
import { createServer } from 'http';
|
|
||||||
import { parse } from 'url';
|
|
||||||
import next from 'next';
|
|
||||||
import { DataSource, EntityTarget } from 'typeorm';
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
import { initializeDataSource } from './src/data-source';
|
|
||||||
import { Payment } from './src/entity/Payment';
|
|
||||||
import { Tweet } from './src/entity/Tweet';
|
|
||||||
|
|
||||||
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: QuotesService
|
|
||||||
appDataSource: DataSource
|
|
||||||
entities: { [key: string]: EntityTarget<any>}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Look for a better way to use quotesService
|
|
||||||
// Initialize global quotes service
|
|
||||||
global.quotesService = quotesService
|
|
||||||
|
|
||||||
global.entities = {
|
|
||||||
Payment,
|
|
||||||
Tweet
|
|
||||||
};
|
|
||||||
|
|
||||||
app.prepare().then(async() => {
|
|
||||||
global.appDataSource = await initializeDataSource();
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
@ -1,10 +1,5 @@
|
|||||||
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 { verifyPayment, markSignatureAsUsed } from '../../../utils/verifyPayment';
|
|
||||||
import { uploadToPinata } from '../../../utils/uploadToPinata';
|
|
||||||
|
|
||||||
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')
|
||||||
@ -21,9 +16,7 @@ const IMAGE_HEIGHT: number = 1024
|
|||||||
|
|
||||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||||
try {
|
try {
|
||||||
const { prompt, modelId, transactionSignature } = await req.json();
|
const { prompt, modelId } = await req.json()
|
||||||
const host = req.headers.get("host"); // Get the hostname from request headers
|
|
||||||
const protocol = req.headers.get("x-forwarded-proto") || "http"; // Handle reverse proxies
|
|
||||||
|
|
||||||
if (!prompt || !modelId) {
|
if (!prompt || !modelId) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -32,39 +25,6 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transactionSignature) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Transaction signature is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = FLUX_MODELS.find((model) => model.modelId === modelId)
|
|
||||||
if (!model) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid modelId' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
{ error: 'Payment verification failed or transaction signature has already been used' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await markSignatureAsUsed(transactionSignature);
|
|
||||||
|
|
||||||
console.log('Generating with Flux model:', modelId)
|
console.log('Generating with Flux model:', modelId)
|
||||||
console.log('Prompt:', prompt)
|
console.log('Prompt:', prompt)
|
||||||
|
|
||||||
@ -91,31 +51,14 @@ 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)
|
||||||
return NextResponse.json(
|
throw new Error('No image URL in response')
|
||||||
{ error: 'No image URL in response: ', result },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinataResult = await uploadToPinata(imageUrl, transactionSignature);
|
return NextResponse.json({ imageUrl })
|
||||||
|
|
||||||
if (pinataResult.error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to upload image to Pinata' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract CID from the URL
|
|
||||||
const cid = pinataResult.imageUrl!.split("/ipfs/")[1];
|
|
||||||
|
|
||||||
const publicUrl = `${protocol}://${host}/api/image/${cid}`;
|
|
||||||
|
|
||||||
return NextResponse.json({ imageUrl: publicUrl })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Flux generation error:', error)
|
console.error('Flux generation error:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to generate image' },
|
{ error: error instanceof Error ? error.message : 'Failed to generate image' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function GET(req: NextRequest, { params }: { params: { cid?: string } }) {
|
|
||||||
const { cid } = params;
|
|
||||||
|
|
||||||
if (!cid) {
|
|
||||||
return NextResponse.json({ error: 'CID is required' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pinataUrl = `https://${process.env.PINATA_GATEWAY}/ipfs/${cid}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(pinataUrl);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return NextResponse.json({ error: 'Failed to fetch from Pinata' }, { status: response.status });
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
|
|
||||||
return new NextResponse(buffer, {
|
|
||||||
status: 200,
|
|
||||||
headers: { 'Content-Type': contentType },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching from Pinata:', error);
|
|
||||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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,12 +0,0 @@
|
|||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
import { processTweet, verifySignatureInTweet } from '../../../utils/verifyTweet';
|
|
||||||
import { extractData } from '../../../utils/tweetMessage';
|
|
||||||
|
|
||||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
|
||||||
try {
|
|
||||||
const { tweetUrl, secret } = await req.json();
|
|
||||||
|
|
||||||
if(secret !== process.env.VERIFY_TWEET_SECRET) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Access denied' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `https://publish.twitter.com/oembed?url=${tweetUrl}&maxwidth=600`;
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const { handle, txSignature, memeUrl } = extractData(data.html);
|
|
||||||
|
|
||||||
if (!handle || !txSignature) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Could not extract tweet data' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSigVerified = await verifySignatureInTweet(txSignature);
|
|
||||||
const isHandleCorrect = handle === process.env.NEXT_PUBLIC_TWITTER_HANDLE;
|
|
||||||
|
|
||||||
// TODO: Verify dynamic page URL in tweet
|
|
||||||
const isVerified = isSigVerified && isHandleCorrect;
|
|
||||||
if (!isVerified) {
|
|
||||||
throw new Error('Tweet is not valid');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify and store valid tweet
|
|
||||||
const result = await processTweet(txSignature, memeUrl);
|
|
||||||
|
|
||||||
return NextResponse.json(result)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error while verifying tweet:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import type { Metadata, ResolvingMetadata } from 'next';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
params: { id: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMeme(id: string): string {
|
|
||||||
const memeUrl = `${process.env.SITE_URL}/api/image/${id}`
|
|
||||||
return memeUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata(
|
|
||||||
{ params }: Props,
|
|
||||||
parent: ResolvingMetadata
|
|
||||||
): Promise<Metadata> {
|
|
||||||
const baseUrl = process.env.SITE_URL!;
|
|
||||||
const meme = getMeme(params.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
metadataBase: new URL(baseUrl),
|
|
||||||
openGraph: {
|
|
||||||
type: 'website',
|
|
||||||
title: 'Generated Meme',
|
|
||||||
url: `${baseUrl}/memes/${params.id}`,
|
|
||||||
images: [{ url: meme }],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
title: 'Generated Meme',
|
|
||||||
images: [{ url: meme }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MemePage({ params }: Props) {
|
|
||||||
const meme = getMeme(params.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
|
||||||
<div className="relative flex flex-col items-center">
|
|
||||||
<div className="relative w-full max-w-2xl aspect-square">
|
|
||||||
<img
|
|
||||||
src={meme}
|
|
||||||
alt='Generated meme'
|
|
||||||
className="object-contain rounded-lg w-full h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
123
src/app/page.tsx
123
src/app/page.tsx
@ -1,14 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState } from 'react'
|
||||||
import Big from 'big.js';
|
|
||||||
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'
|
||||||
import { processMTMPayment } from '../services/paymentService'
|
import { processMTMPayment } from '../services/paymentService'
|
||||||
import { connectWallet, disconnectWallet, WalletState } from '../services/walletService'
|
import { connectWallet, WalletState } from '../services/walletService'
|
||||||
import { WalletType } from '../services/types'
|
import { WalletType } from '../services/types'
|
||||||
|
|
||||||
const Page: React.FC = (): React.ReactElement => {
|
const Page: React.FC = (): React.ReactElement => {
|
||||||
@ -18,26 +15,7 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
type: null
|
type: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const [priceMTMFor1USDC, setPriceMTMFor1USDC] = useState<BN>(new BN(0));
|
const handleConnect = async (walletType: WalletType): Promise<void> => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchPrice = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/quotes');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Convert the string back to BN
|
|
||||||
const price = new BN(data.latestMTMAmount);
|
|
||||||
setPriceMTMFor1USDC(price);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch price:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchPrice();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleConnect = async (walletType: WalletType): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const newWalletState = await connectWallet(walletType)
|
const newWalletState = await connectWallet(walletType)
|
||||||
setWalletState(newWalletState)
|
setWalletState(newWalletState)
|
||||||
@ -51,63 +29,26 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDisconnect = async (): Promise<void> => {
|
|
||||||
if (walletState.type) {
|
|
||||||
await disconnectWallet(walletState.type)
|
|
||||||
setWalletState({
|
|
||||||
connected: false,
|
|
||||||
publicKey: null,
|
|
||||||
type: null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roundUpBigNumber = (bnValue: BN): BN => {
|
const handleFluxGeneration = (modelId: string, cost: number) => {
|
||||||
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> => {
|
return async (prompt: string): Promise<FluxGenerationResult> => {
|
||||||
const { connected, publicKey, type } = walletState;
|
if (!walletState.connected || !walletState.publicKey || !window.solflare) {
|
||||||
|
|
||||||
if (!connected || !publicKey || !type ||
|
|
||||||
(type === 'phantom' && !window.phantom) ||
|
|
||||||
(type === 'solflare' && !window.solflare)) {
|
|
||||||
return { error: 'Wallet not connected' }
|
return { error: 'Wallet not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Process payment first
|
||||||
// Convert cost in USDC to MTM tokens using the price ratio
|
const paymentResult = await processMTMPayment(
|
||||||
const paymentResult = await processMTMPayment(
|
walletState.publicKey,
|
||||||
publicKey,
|
cost,
|
||||||
roundUpBigNumber(cost),
|
walletState.type
|
||||||
type
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (!paymentResult.success) {
|
if (!paymentResult.success) {
|
||||||
return { error: paymentResult.error }
|
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' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then generate image with specified model
|
||||||
|
return generateWithFlux(prompt, modelId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,31 +67,22 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
<WalletHeader
|
<WalletHeader
|
||||||
walletState={walletState}
|
walletState={walletState}
|
||||||
onConnect={handleConnect}
|
onConnect={handleConnect}
|
||||||
onDisconnect={handleDisconnect}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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
|
<AIServiceCard
|
||||||
const scaledModelCost = new BN(model.cost * 100);
|
key={model.modelId}
|
||||||
const priceMTM = scaledModelCost.mul(priceMTMFor1USDC).div(new BN(100));
|
title={model.name}
|
||||||
|
description={model.description}
|
||||||
|
tokenCost={model.cost}
|
||||||
|
isWalletConnected={walletState.connected}
|
||||||
|
onGenerate={handleFluxGeneration(model.modelId, model.cost)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
return (
|
|
||||||
<AIServiceCard
|
|
||||||
key={model.modelId}
|
|
||||||
title={model.name}
|
|
||||||
description={model.description}
|
|
||||||
isWalletConnected={walletState.connected}
|
|
||||||
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>
|
||||||
@ -166,6 +98,7 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
<span className="text-orange-300 text-sm">TBD</span>
|
<span className="text-orange-300 text-sm">TBD</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
disabled
|
disabled
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import TweetUrlForm from "../../../components/TweetUrlForm";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
params: { secret: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SubmitTweetPage({params}: Props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row justify-center items-center">
|
|
||||||
<TweetUrlForm secret={params.secret} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,51 +1,36 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import BN from 'bn.js';
|
|
||||||
import Big from 'big.js';
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
|
|
||||||
import { generateTweetText } from '../utils/tweetMessage';
|
|
||||||
|
|
||||||
interface AIServiceCardProps {
|
interface AIServiceCardProps {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
|
tokenCost: number
|
||||||
isWalletConnected: boolean
|
isWalletConnected: boolean
|
||||||
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, transactionSignature?: string, error?: string }>
|
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
|
||||||
priceMTM: BN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GenerationState {
|
interface GenerationState {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
processing: boolean
|
processing: boolean
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
transactionSignature: string | null
|
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUnitToWholeNumber = (value: BN, decimals: number): string => {
|
|
||||||
const bigValue = new Big(value.toString());
|
|
||||||
const factor = new Big(10).pow(decimals);
|
|
||||||
|
|
||||||
return bigValue.div(factor).round(0, Big.roundUp).toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>({
|
||||||
loading: false,
|
loading: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
transactionSignature: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
|
||||||
|
|
||||||
const handleGenerate = async (): Promise<void> => {
|
const handleGenerate = async (): Promise<void> => {
|
||||||
if (!inputText || !isWalletConnected) return
|
if (!inputText || !isWalletConnected) return
|
||||||
@ -65,20 +50,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.imageUrl && result.transactionSignature) {
|
if (result.imageUrl) {
|
||||||
setGenerationState({
|
setGenerationState({
|
||||||
loading: false,
|
loading: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
imageUrl: result.imageUrl,
|
imageUrl: result.imageUrl,
|
||||||
transactionSignature: result.transactionSignature,
|
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -93,16 +72,6 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateTwitterShareUrl = (imageUrl: string, transactionSignature: string): string => {
|
|
||||||
const baseUrl = window.location.href;
|
|
||||||
const cid = imageUrl.split("/image/")[1];
|
|
||||||
const memePageUrl = `${baseUrl}memes/${cid}`;
|
|
||||||
|
|
||||||
const tweetText = generateTweetText(transactionSignature, process.env.NEXT_PUBLIC_TWITTER_HANDLE)
|
|
||||||
|
|
||||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(memePageUrl)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
|
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@ -112,7 +81,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 mt-2">{description}</p>
|
<p className="text-gray-400 mt-2">{description}</p>
|
||||||
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
|
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
|
||||||
Cost: {priceMTM ? baseUnitToWholeNumber(priceMTM, 6) : '...'} MTM
|
Cost: {tokenCost} MTM
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,7 +105,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
||||||
>
|
>
|
||||||
{generationState.loading ? 'Processing...' : `Pay ${priceMTM ? baseUnitToWholeNumber(priceMTM, 6) : '...'} MTM & Generate`}
|
{generationState.loading ? 'Processing...' : `Pay ${tokenCost} MTM & Generate`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -146,27 +115,13 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{generationState.imageUrl && generationState.transactionSignature && (
|
{generationState.imageUrl && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{!isImageLoaded && <p>Loading image...</p>}
|
|
||||||
<img
|
<img
|
||||||
src={generationState.imageUrl}
|
src={generationState.imageUrl}
|
||||||
alt="Generated content"
|
alt="Generated content"
|
||||||
className="w-full h-auto rounded-xl shadow-2xl"
|
className="w-full h-auto rounded-xl shadow-2xl"
|
||||||
onLoad={() => setIsImageLoaded(true)}
|
|
||||||
/>
|
/>
|
||||||
{isImageLoaded &&
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<a
|
|
||||||
href={generateTwitterShareUrl(generationState.imageUrl, generationState.transactionSignature)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-block w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 shadow-lg hover:shadow-blue-500/25"
|
|
||||||
>
|
|
||||||
Share on Twitter
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -174,6 +129,4 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default dynamic(() => Promise.resolve(AIServiceCard), {
|
export default AIServiceCard
|
||||||
ssr: false
|
|
||||||
})
|
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
|
|
||||||
const TweetUrlForm: React.FC<{secret: string}> = ({secret}) => {
|
|
||||||
const [inputText, setInputText] = useState<string>('')
|
|
||||||
|
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
||||||
const [escrow, setEscrow] = useState('');
|
|
||||||
|
|
||||||
const handleSubmit = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const response = await fetch('/api/tweet', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ tweetUrl: inputText, secret }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const parsedResponse = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setSubmitError(parsedResponse.error);
|
|
||||||
throw new Error(`Failed to submit tweet: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedResponse.data && parsedResponse.data.escrow) {
|
|
||||||
setEscrow(parsedResponse.data.escrow);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
setSubmitError(error.message);
|
|
||||||
console.error('Failed to submit tweet:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSubmitted(true);
|
|
||||||
setLoading(false);
|
|
||||||
setInputText('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8 mx-auto my-10">
|
|
||||||
<div className="p-6">
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{isSubmitted ? submitError ? <p className='text-red-500'>Submission failed: {submitError}</p> : <p className='text-green-500'>Tweet submitted!</p> : <></>}
|
|
||||||
{escrow && <p className='text-green-500'>Lock created successfully: {escrow}</p>}
|
|
||||||
<textarea
|
|
||||||
value={inputText}
|
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
|
||||||
placeholder="Enter the tweet URL here..."
|
|
||||||
className="w-full bg-gray-900/50 text-gray-100 border border-gray-700 rounded-xl p-4
|
|
||||||
placeholder-gray-500 focus:border-green-500 focus:ring-2 focus:ring-green-500/20
|
|
||||||
focus:outline-none min-h-[120px] transition-all duration-200
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600
|
|
||||||
hover:to-emerald-600 text-white font-semibold py-4 px-6 rounded-xl
|
|
||||||
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
|
||||||
>
|
|
||||||
{loading ? 'Submitting...' : 'Submit' }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TweetUrlForm
|
|
@ -6,10 +6,9 @@ import { WalletState, SUPPORTED_WALLETS } from '../services/walletService'
|
|||||||
interface WalletHeaderProps {
|
interface WalletHeaderProps {
|
||||||
walletState: WalletState
|
walletState: WalletState
|
||||||
onConnect: (walletType: string) => Promise<void>
|
onConnect: (walletType: string) => Promise<void>
|
||||||
onDisconnect: () => Promise<void>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WalletHeader: React.FC<WalletHeaderProps> = ({ walletState, onConnect, onDisconnect }) => {
|
const WalletHeader: React.FC<WalletHeaderProps> = ({ walletState, onConnect }) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-slate-900/50 backdrop-blur-lg rounded-xl shadow-lg border border-orange-800/50 mb-8 p-4">
|
<div className="w-full bg-slate-900/50 backdrop-blur-lg rounded-xl shadow-lg border border-orange-800/50 mb-8 p-4">
|
||||||
{!walletState.connected ? (
|
{!walletState.connected ? (
|
||||||
@ -27,17 +26,11 @@ const WalletHeader: React.FC<WalletHeaderProps> = ({ walletState, onConnect, onD
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-slate-300 mb-2 sm:mb-0">Connected Wallet</span>
|
<span className="text-slate-300">Connected Wallet</span>
|
||||||
<span className="px-3 py-1 bg-amber-500/20 rounded-full text-amber-200 text-sm mb-2 sm:mb-0">
|
<span className="px-3 py-1 bg-amber-500/20 rounded-full text-amber-200 text-sm">
|
||||||
{walletState.publicKey?.slice(0, 22)}...
|
{walletState.publicKey?.slice(0, 22)}...
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
onClick={onDisconnect}
|
|
||||||
className="bg-red-500 hover:bg-red-600 text-white font-semibold py-2 px-4 rounded-lg transition-all duration-200 shadow-lg hover:shadow-red-500/25"
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { DataSource } from 'typeorm';
|
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
export async function initializeDataSource() {
|
|
||||||
try {
|
|
||||||
const DB_PATH = process.env.DB_PATH;
|
|
||||||
assert(DB_PATH, 'DB_PATH not set');
|
|
||||||
|
|
||||||
console.log('Initializing Data Source');
|
|
||||||
|
|
||||||
const appDataSource = new DataSource({
|
|
||||||
type: 'sqlite',
|
|
||||||
database: DB_PATH,
|
|
||||||
synchronize: true,
|
|
||||||
logging: false,
|
|
||||||
entities: [global.entities.Payment, global.entities.Tweet],
|
|
||||||
migrations: [],
|
|
||||||
subscribers: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
await appDataSource.initialize();
|
|
||||||
return appDataSource;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error during Data Source initialization:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export class Payment {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column({ unique: true })
|
|
||||||
transactionSignature!: string;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export class Tweet {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column({ type: 'text', unique: true, nullable: true })
|
|
||||||
url!: string | null;
|
|
||||||
|
|
||||||
@Column({ unique: true })
|
|
||||||
transactionSignature!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', nullable: true })
|
|
||||||
isLockCreated!: boolean | null;
|
|
||||||
|
|
||||||
@Column({ type: 'text', unique: true, nullable: true })
|
|
||||||
lockEscrow!: string | null;
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
assert(process.env.MAX_RETRIES_FOR_LOCK_TX);
|
|
||||||
|
|
||||||
const connection = new Connection(process.env.NEXT_PUBLIC_SOLANA_RPC_URL);
|
|
||||||
const MAX_RETRIES = Number(process.env.MAX_RETRIES_FOR_LOCK_TX);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
let attempt = 0;
|
|
||||||
|
|
||||||
while (attempt < MAX_RETRIES) {
|
|
||||||
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,
|
|
||||||
} as any)
|
|
||||||
.remainingAccounts(remainingAccounts ? remainingAccounts : [])
|
|
||||||
.preInstructions([
|
|
||||||
createAssociatedTokenAccountInstruction(
|
|
||||||
ownerKeypair.publicKey,
|
|
||||||
escrowToken,
|
|
||||||
escrow,
|
|
||||||
tokenMint,
|
|
||||||
tokenProgram,
|
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
||||||
),
|
|
||||||
])
|
|
||||||
.signers([baseKP, ownerKeypair])
|
|
||||||
.rpc();
|
|
||||||
|
|
||||||
return escrow; // Success, return escrow
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Attempt ${attempt + 1} Transaction confirmation delayed`);
|
|
||||||
|
|
||||||
// If transaction expired, check confirmation
|
|
||||||
if (error instanceof TransactionExpiredTimeoutError) {
|
|
||||||
console.log('Checking if transaction was confirmed...');
|
|
||||||
const confirmedTransaction = await program.provider.connection.getTransaction(error.signature, {
|
|
||||||
commitment: 'confirmed',
|
|
||||||
maxSupportedTransactionVersion: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
if (confirmedTransaction !== null) {
|
|
||||||
return escrow;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt++;
|
|
||||||
console.log(`Attempt ${attempt + 1} Transaction failed, retrying...`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Transaction failed after maximum retries");
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
// 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];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
export interface FluxGenerationResult {
|
export interface FluxGenerationResult {
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
error?: string
|
error?: string
|
||||||
transactionSignature?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FluxModelConfig {
|
export interface FluxModelConfig {
|
||||||
@ -17,26 +16,25 @@ export const FLUX_MODELS: FluxModelConfig[] = [
|
|||||||
modelId: "fal-ai/flux/schnell",
|
modelId: "fal-ai/flux/schnell",
|
||||||
name: "Schnell",
|
name: "Schnell",
|
||||||
description: "Fast meme generator",
|
description: "Fast meme generator",
|
||||||
cost: 0.05
|
cost: 300
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
modelId: "fal-ai/recraft-v3",
|
modelId: "fal-ai/recraft-v3",
|
||||||
name: "Recraft",
|
name: "Recraft",
|
||||||
description: "Advanced meme generator",
|
description: "Advanced meme generator",
|
||||||
cost: 0.10
|
cost: 400
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
modelId: "fal-ai/stable-diffusion-v35-large",
|
modelId: "fal-ai/stable-diffusion-v35-large",
|
||||||
name: "Marquee",
|
name: "Marquee",
|
||||||
description: "Best meme generator",
|
description: "Best meme generator",
|
||||||
cost: 0.15
|
cost: 500
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export async function generateWithFlux(
|
export async function generateWithFlux(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
modelId: string,
|
modelId: string
|
||||||
transactionSignature: string,
|
|
||||||
): Promise<FluxGenerationResult> {
|
): Promise<FluxGenerationResult> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/flux', {
|
const response = await fetch('/api/flux', {
|
||||||
@ -46,20 +44,19 @@ export async function generateWithFlux(
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
prompt,
|
prompt,
|
||||||
modelId,
|
modelId
|
||||||
transactionSignature,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to generate image, reloading...')
|
throw new Error('Failed to generate image')
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
console.log('Raw Flux response:', data)
|
console.log('Raw Flux response:', data)
|
||||||
|
|
||||||
if (data.imageUrl) {
|
if (data.imageUrl) {
|
||||||
return { imageUrl: data.imageUrl, transactionSignature }
|
return { imageUrl: data.imageUrl }
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected response structure:', data)
|
console.error('Unexpected response structure:', data)
|
||||||
throw new Error('Invalid response format from Flux API')
|
throw new Error('Invalid response format from Flux API')
|
||||||
|
@ -1,24 +1,17 @@
|
|||||||
import assert from 'assert';
|
import { Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'
|
||||||
import BN from 'bn.js';
|
|
||||||
|
|
||||||
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
|
|
||||||
import {
|
import {
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
createTransferInstruction,
|
createTransferInstruction,
|
||||||
|
getAssociatedTokenAddress,
|
||||||
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');
|
const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump'
|
||||||
assert(process.env.NEXT_PUBLIC_MTM_MINT_ADDRESS, 'MTM_MINT_ADDRESS is required');
|
const PAYMENT_RECEIVER_ADDRESS: string = 'FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY'
|
||||||
assert(process.env.NEXT_PUBLIC_MTM_RECIPIENT_MULTISIG_ADDRESS, 'MTM_RECIPIENT_MULTISIG_ADDRESS is required');
|
const SOLANA_RPC_URL: string = 'https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
|
||||||
|
const SOLANA_WEBSOCKET_URL: string = 'wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
|
||||||
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;
|
|
||||||
|
|
||||||
const connection = new Connection(
|
const connection = new Connection(
|
||||||
SOLANA_RPC_URL,
|
SOLANA_RPC_URL,
|
||||||
@ -31,7 +24,6 @@ const connection = new Connection(
|
|||||||
|
|
||||||
export interface PaymentResult {
|
export interface PaymentResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
transactionSignature?: string
|
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +47,7 @@ interface WalletAdapter {
|
|||||||
|
|
||||||
export async function processMTMPayment(
|
export async function processMTMPayment(
|
||||||
walletPublicKey: string,
|
walletPublicKey: string,
|
||||||
tokenAmount: BN,
|
tokenAmount: number,
|
||||||
walletType: WalletType
|
walletType: WalletType
|
||||||
): Promise<PaymentResult> {
|
): Promise<PaymentResult> {
|
||||||
try {
|
try {
|
||||||
@ -127,14 +119,12 @@ export async function processMTMPayment(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountToSend = BigInt(tokenAmount.toString());
|
|
||||||
|
|
||||||
transaction.add(
|
transaction.add(
|
||||||
createTransferInstruction(
|
createTransferInstruction(
|
||||||
senderATA,
|
senderATA,
|
||||||
receiverATA,
|
receiverATA,
|
||||||
senderPublicKey,
|
senderPublicKey,
|
||||||
amountToSend
|
BigInt(tokenAmount * (10 ** 6))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,7 +147,7 @@ export async function processMTMPayment(
|
|||||||
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
|
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, transactionSignature: signature };
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Payment error:', error)
|
console.error('Payment error:', error)
|
||||||
return {
|
return {
|
||||||
|
@ -10,7 +10,6 @@ export interface WalletConfig {
|
|||||||
type: WalletType
|
type: WalletType
|
||||||
name: string
|
name: string
|
||||||
connect: () => Promise<{ publicKey: string } | null>
|
connect: () => Promise<{ publicKey: string } | null>
|
||||||
disconnect: () => Promise<void>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectSolflare = async (): Promise<{ publicKey: string } | null> => {
|
const connectSolflare = async (): Promise<{ publicKey: string } | null> => {
|
||||||
@ -19,12 +18,6 @@ const connectSolflare = async (): Promise<{ publicKey: string } | null> => {
|
|||||||
return window.solflare.publicKey ? { publicKey: window.solflare.publicKey.toString() } : null
|
return window.solflare.publicKey ? { publicKey: window.solflare.publicKey.toString() } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const disconnectSolflare = async (): Promise<void> => {
|
|
||||||
if (window.solflare) {
|
|
||||||
await window.solflare.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectPhantom = async (): Promise<{ publicKey: string } | null> => {
|
const connectPhantom = async (): Promise<{ publicKey: string } | null> => {
|
||||||
if (!window.phantom?.solana) return null
|
if (!window.phantom?.solana) return null
|
||||||
try {
|
try {
|
||||||
@ -35,24 +28,16 @@ const connectPhantom = async (): Promise<{ publicKey: string } | null> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disconnectPhantom = async (): Promise<void> => {
|
|
||||||
if (window.phantom?.solana) {
|
|
||||||
await window.phantom.solana.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SUPPORTED_WALLETS: WalletConfig[] = [
|
export const SUPPORTED_WALLETS: WalletConfig[] = [
|
||||||
{
|
{
|
||||||
type: 'solflare',
|
type: 'solflare',
|
||||||
name: 'Solflare',
|
name: 'Solflare',
|
||||||
connect: connectSolflare,
|
connect: connectSolflare
|
||||||
disconnect: disconnectSolflare
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'phantom',
|
type: 'phantom',
|
||||||
name: 'Phantom',
|
name: 'Phantom',
|
||||||
connect: connectPhantom,
|
connect: connectPhantom
|
||||||
disconnect: disconnectPhantom
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -77,10 +62,3 @@ export async function connectWallet(type: WalletType): Promise<WalletState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disconnectWallet(type: WalletType): Promise<void> {
|
|
||||||
const wallet = SUPPORTED_WALLETS.find(w => w.type === type)
|
|
||||||
if (wallet) {
|
|
||||||
await wallet.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
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, amount: BN): Promise<anchor.web3.PublicKey | void> {
|
|
||||||
|
|
||||||
if (amount.eq(new BN(0))) {
|
|
||||||
console.log('Invalid Amount');
|
|
||||||
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: amount, // 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 createRewardLock(recipient: string, txMtmAmount: 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 for creating reward wSOL lock');
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockDuration = 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(recipient);
|
|
||||||
|
|
||||||
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(txMtmAmount).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, lockDuration, wsolAmount);
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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 };
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,28 +0,0 @@
|
|||||||
import { PinataSDK } from 'pinata-web3';
|
|
||||||
import 'dotenv/config';
|
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
import { FluxGenerationResult } from '../services/fluxService';
|
|
||||||
|
|
||||||
assert(process.env.PINATA_JWT, "PINATA_JWT is required");
|
|
||||||
assert(process.env.PINATA_GATEWAY, "PINATA_GATEWAY is required");
|
|
||||||
|
|
||||||
const pinata = new PinataSDK({
|
|
||||||
pinataJwt: process.env.PINATA_JWT,
|
|
||||||
pinataGateway: process.env.PINATA_GATEWAY,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function uploadToPinata(imageUrl: string, transactionSignature: string): Promise<FluxGenerationResult> {
|
|
||||||
try {
|
|
||||||
const upload = await pinata.upload.url(imageUrl, { metadata: { name: transactionSignature }});
|
|
||||||
|
|
||||||
const publicURL = await pinata.gateways.convert(upload.IpfsHash);
|
|
||||||
|
|
||||||
return { imageUrl: publicURL };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading to Pinata:', error)
|
|
||||||
return {
|
|
||||||
error: error instanceof Error ? error.message : 'Upload failed'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
import assert from 'assert';
|
|
||||||
import BN from 'bn.js';
|
|
||||||
|
|
||||||
import { Connection } from '@solana/web3.js';
|
|
||||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
||||||
import { extractTxInfo } from './extractTxInfo';
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const connection = new Connection(
|
|
||||||
SOLANA_RPC_URL,
|
|
||||||
{
|
|
||||||
commitment: 'confirmed',
|
|
||||||
wsEndpoint: SOLANA_WEBSOCKET_URL,
|
|
||||||
confirmTransactionInitialTimeout: 60000,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function isSignatureUsed(transactionSignature: string): Promise<boolean> {
|
|
||||||
const paymentRepository = global.appDataSource.getRepository(global.entities.Payment);
|
|
||||||
const payment = await paymentRepository.findOneBy({ transactionSignature });
|
|
||||||
if (payment) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function markSignatureAsUsed(transactionSignature: string): Promise<void> {
|
|
||||||
await global.appDataSource.transaction(async (transactionalEntityManager) => {
|
|
||||||
const paymentRepository = transactionalEntityManager.getRepository(global.entities.Payment);
|
|
||||||
|
|
||||||
// Check if the payment with the given signature already exists
|
|
||||||
const exists = await paymentRepository.exists({ where: { transactionSignature } });
|
|
||||||
|
|
||||||
// If not, create a new payment entry
|
|
||||||
if (!exists) {
|
|
||||||
const newPayment = paymentRepository.create({ transactionSignature });
|
|
||||||
await paymentRepository.save(newPayment);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Verify that payment receiver is correct
|
|
||||||
export async function verifyPayment(
|
|
||||||
transactionSignature: string,
|
|
||||||
tokenAmount: BN,
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
// Check if the signature is already used
|
|
||||||
if (await isSignatureUsed(transactionSignature)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { amount } = await extractTxInfo(transactionSignature);
|
|
||||||
|
|
||||||
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);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
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);
|
|
||||||
const payment = await paymentRepository.findOneBy({ transactionSignature });
|
|
||||||
|
|
||||||
if (!payment) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tweetRepository = global.appDataSource.getRepository(global.entities.Tweet);
|
|
||||||
const tweet = await tweetRepository.findOneBy({ transactionSignature });
|
|
||||||
|
|
||||||
if (tweet) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
57
styles.css
57
styles.css
@ -1,57 +0,0 @@
|
|||||||
/* ...existing code... */
|
|
||||||
|
|
||||||
/* Mobile styles */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
body {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.header, .footer {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
/* Add more styles as needed for mobile responsiveness */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tablet styles */
|
|
||||||
@media (min-width: 601px) and (max-width: 1024px) {
|
|
||||||
body {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.header, .footer {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
/* Add more styles as needed for tablet responsiveness */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desktop styles */
|
|
||||||
@media (min-width: 1025px) {
|
|
||||||
body {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
.header, .footer {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
/* Add more styles as needed for desktop responsiveness */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ...existing code... */
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -16,14 +16,11 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"strictNullChecks": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
// 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"]
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user