Remove code for ATOM token payments
This commit is contained in:
parent
982232545a
commit
4c291dc307
@ -1,16 +1,14 @@
|
||||
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
|
||||
|
||||
# ATOM Payment Configuration
|
||||
NEXT_PUBLIC_RECIPIENT_ADDRESS=cosmos1yourrealaddress
|
||||
NEXT_PUBLIC_COSMOS_RPC_URL=https://rpc.cosmos.network
|
||||
NEXT_PUBLIC_COSMOS_API_URL=https://api.cosmos.network
|
||||
NEXT_PUBLIC_COSMOS_CHAIN_ID=cosmoshub-4
|
||||
|
||||
# Solana/GOR Payment Configuration
|
||||
# Solana Token Payment Configuration
|
||||
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/
|
||||
NEXT_PUBLIC_GOR_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
|
||||
NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS=
|
||||
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
|
||||
NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=
|
||||
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
|
||||
NEXT_PUBLIC_SOLANA_TOKEN_NAME=GOR Token
|
||||
NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS=6
|
||||
NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT=50
|
||||
|
||||
# UI Configuration (optional)
|
||||
NEXT_PUBLIC_DOMAIN_SUFFIX=
|
||||
@ -30,11 +28,9 @@ REGISTRY_FEES=900000alnt
|
||||
REGISTRY_GAS_PRICE=0.001
|
||||
|
||||
# Application Configuration
|
||||
APP_NAME=atom-deploy
|
||||
APP_NAME=gor-deploy
|
||||
DEPLOYER_LRN=
|
||||
|
||||
# LNT Transfer Configuration (required for both ATOM and GOR flows)
|
||||
# LNT Transfer Configuration (required for Solana flow)
|
||||
# Note: REGISTRY_USER_KEY is used as the prefilled account for LNT transfers
|
||||
# TODO: Use deployer lrn to determine the address
|
||||
LACONIC_SERVICE_PROVIDER_ADDRESS=
|
||||
LACONIC_TRANSFER_AMOUNT=1000000alnt
|
79
README.md
79
README.md
@ -1,13 +1,15 @@
|
||||
# ATOM Deploy - Laconic Registry
|
||||
# GOR Deploy - Laconic Registry
|
||||
|
||||
A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using Keplr wallet) and paste a URL. The transaction hash and URL are used to create records in the Laconic Registry.
|
||||
A simple Next.js frontend that allows users to pay in GOR tokens (configurable Solana SPL tokens) using Solana wallets and paste a URL. The transaction hash and URL are used to create records in the Laconic Registry.
|
||||
|
||||
## Features
|
||||
|
||||
- Keplr wallet integration for ATOM payments
|
||||
- Solana wallet integration (Phantom & Solflare) for GOR token payments
|
||||
- Configurable Solana SPL token support (defaults to GOR)
|
||||
- URL validation and submission
|
||||
- Transaction verification
|
||||
- Solana transaction verification with replay protection
|
||||
- Laconic Registry record creation using official `@cerc-io/registry-sdk`
|
||||
- LNT token transfer integration for registry payments
|
||||
- Automatic salt addition to DNS names to prevent collisions
|
||||
- Error handling and validation throughout the application flow
|
||||
|
||||
@ -15,7 +17,7 @@ A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using
|
||||
|
||||
- Node.js 18.x or later
|
||||
- npm or yarn
|
||||
- Keplr wallet browser extension
|
||||
- Solana wallet browser extension (Phantom or Solflare)
|
||||
- Access to a Laconic Registry node
|
||||
|
||||
## Environment Variables
|
||||
@ -29,10 +31,14 @@ cp .env.local.example .env.local
|
||||
Required environment variables:
|
||||
|
||||
Client-side (must be prefixed with NEXT_PUBLIC_):
|
||||
- `NEXT_PUBLIC_RECIPIENT_ADDRESS` - The Cosmos address that will receive ATOM payments
|
||||
- `NEXT_PUBLIC_COSMOS_RPC_URL` - The RPC URL for the Cosmos blockchain (used by Keplr for transactions)
|
||||
- `NEXT_PUBLIC_COSMOS_API_URL` - The REST API URL for the Cosmos blockchain (used for transaction queries)
|
||||
- `NEXT_PUBLIC_COSMOS_CHAIN_ID` - The chain ID for Keplr wallet (e.g., cosmoshub-4)
|
||||
- `NEXT_PUBLIC_SOLANA_RPC_URL` - The RPC URL for the Solana blockchain
|
||||
- `NEXT_PUBLIC_SOLANA_WEBSOCKET_URL` - The WebSocket URL for Solana (optional)
|
||||
- `NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS` - The mint address of the SPL token to accept
|
||||
- `NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS` - The Solana address that will receive token payments
|
||||
- `NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL` - The token symbol to display (e.g., "GOR")
|
||||
- `NEXT_PUBLIC_SOLANA_TOKEN_NAME` - The full token name (e.g., "GOR Token")
|
||||
- `NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS` - The number of decimals for the token (e.g., 6)
|
||||
- `NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT` - The fixed payment amount required (e.g., 50)
|
||||
- `NEXT_PUBLIC_DOMAIN_SUFFIX` - Optional suffix to append to DNS names in the UI (e.g. ".example.com")
|
||||
- `NEXT_PUBLIC_EXAMPLE_URL` - Example URL to pre-fill in the URL form (e.g. "https://github.com/cerc-io/laconic-registry-cli")
|
||||
|
||||
@ -42,9 +48,11 @@ Server-side:
|
||||
- `REGISTRY_RPC_ENDPOINT` - The RPC endpoint for the Laconic Registry
|
||||
- `REGISTRY_BOND_ID` - The bond ID to use for Laconic Registry records
|
||||
- `REGISTRY_AUTHORITY` - The authority for Laconic Registry LRNs
|
||||
- `REGISTRY_USER_KEY` - The private key for Laconic Registry transactions
|
||||
- `APP_NAME` - The name of the application (used in record creation)
|
||||
- `REGISTRY_USER_KEY` - The private key for Laconic Registry transactions (also used for LNT transfers)
|
||||
- `APP_NAME` - The name of the application (used in record creation, defaults to "gor-deploy")
|
||||
- `DEPLOYER_LRN` - The LRN of the deployer
|
||||
- `LACONIC_SERVICE_PROVIDER_ADDRESS` - The Laconic address to receive LNT transfers
|
||||
- `LACONIC_TRANSFER_AMOUNT` - The amount of LNT to transfer (e.g., "1000000alnt")
|
||||
|
||||
## Installation
|
||||
|
||||
@ -74,24 +82,25 @@ npm start
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User connects their Keplr wallet to the application
|
||||
1. User connects their Solana wallet (Phantom or Solflare) to the application
|
||||
2. User enters a URL they want to deploy to the Laconic Registry
|
||||
3. User completes payment in ATOM to a specified address
|
||||
4. The application verifies the transaction using the Cosmos RPC
|
||||
5. The application calls a server-side API route which creates records in the Laconic Registry
|
||||
6. The server generates a unique DNS name by adding a random salt to prevent name collisions
|
||||
7. Two records are created in the Laconic Registry:
|
||||
3. User completes payment in GOR tokens (or configured SPL token) to a specified Solana address
|
||||
4. The application verifies the Solana transaction with replay protection
|
||||
5. After payment verification, the server transfers LNT tokens from a prefilled account to the service provider
|
||||
6. The application calls a server-side API route which creates records in the Laconic Registry using the LNT transfer hash
|
||||
7. The server generates a unique DNS name by adding a random salt to prevent name collisions
|
||||
8. Two records are created in the Laconic Registry:
|
||||
- An ApplicationRecord containing metadata about the URL
|
||||
- An ApplicationDeploymentRequest linking the URL, DNS, and payment transaction
|
||||
- An ApplicationDeploymentRequest linking the URL, DNS, and payment details with external_payment metadata
|
||||
|
||||
### Architecture
|
||||
|
||||
This application uses a hybrid client/server approach:
|
||||
|
||||
- Client-side: Handles the user interface, Keplr wallet integration, and transaction verification
|
||||
- Server-side: Next.js API route handles the communication with the Laconic Registry
|
||||
- Client-side: Handles the user interface, Solana wallet integration, and transaction verification
|
||||
- Server-side: Next.js API route handles LNT transfers and communication with the Laconic Registry
|
||||
|
||||
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience.
|
||||
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience. The dual-payment system (Solana → LNT → Registry) enables cross-chain payment acceptance.
|
||||
|
||||
### Resource Name Formats
|
||||
|
||||
@ -115,7 +124,7 @@ The Laconic Resource Names (LRNs) are generated with the following format:
|
||||
lrn://{authority}/applications/{app-name}-{short-commit-hash}-{random-salt}
|
||||
```
|
||||
|
||||
For example: `lrn://atom/applications/github-abc123-xyz789`
|
||||
For example: `lrn://gor/applications/github-abc123-xyz789`
|
||||
|
||||
Including the commit hash and salt in the LRN ensures that each application record has a unique identifier, consistently matching the DNS naming pattern.
|
||||
|
||||
@ -123,7 +132,7 @@ Including the commit hash and salt in the LRN ensures that each application reco
|
||||
|
||||
This application was built with reference to:
|
||||
- `snowballtools-base/packages/backend/src/registry.ts`
|
||||
- `hosted-frontends/deploy-atom.sh`
|
||||
- Original `hosted-frontends/deploy-atom.sh` (adapted for Solana/GOR)
|
||||
|
||||
## Deployment to Production
|
||||
|
||||
@ -158,24 +167,32 @@ CMD ["npm", "start"]
|
||||
Build and run the Docker container:
|
||||
|
||||
```bash
|
||||
docker build -t atom-deploy .
|
||||
docker run -p 3000:3000 --env-file .env.production atom-deploy
|
||||
docker build -t gor-deploy .
|
||||
docker run -p 3000:3000 --env-file .env.production gor-deploy
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
- You may see a deprecated Buffer() warning during build. This comes from dependencies in the registry-sdk. This doesn't affect functionality.
|
||||
- If using a custom Cosmos chain, ensure that your RPC endpoint supports CORS for client-side requests.
|
||||
- The Keplr wallet integration requires HTTPS in production environments.
|
||||
- Ensure that your Solana RPC endpoint supports CORS for client-side requests.
|
||||
- Solana wallet integrations require HTTPS in production environments.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Keplr Wallet Issues
|
||||
### Solana Wallet Issues
|
||||
|
||||
- **Keplr not detecting**: Install the Keplr browser extension and refresh the page.
|
||||
- **Chain not found in Keplr**: The application will attempt to suggest the chain to Keplr, but if that fails, you may need to manually add the chain in your Keplr wallet settings.
|
||||
- **Wallet not detecting**: Install the Phantom or Solflare browser extension and refresh the page.
|
||||
- **Connection issues**: Ensure the wallet is unlocked and try refreshing the page.
|
||||
- **Transaction failures**: Check that you have sufficient SOL for transaction fees and enough tokens for the payment.
|
||||
|
||||
### Token Configuration
|
||||
|
||||
- **Wrong token**: Verify the `NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS` matches your desired SPL token.
|
||||
- **Incorrect decimals**: Ensure `NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS` matches the token's actual decimal count.
|
||||
- **Payment amount**: Adjust `NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT` to the desired payment amount.
|
||||
|
||||
### Laconic Registry Issues
|
||||
|
||||
- **Failed to create record**: Check that your REGISTRY_USER_KEY and REGISTRY_BOND_ID are correctly set.
|
||||
- **Transaction verification errors**: Ensure your COSMOS_RPC_URL and COSMOS_API_URL are accessible and return correct transaction data.
|
||||
- **LNT transfer errors**: Ensure your REGISTRY_USER_KEY has sufficient LNT balance and the LACONIC_SERVICE_PROVIDER_ADDRESS is valid.
|
||||
- **Transaction verification errors**: Ensure your SOLANA_RPC_URL is accessible and returns correct transaction data.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "atom-deploy",
|
||||
"name": "gor-deploy",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -11,11 +11,9 @@
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@cosmjs/stargate": "^0.32.3",
|
||||
"@keplr-wallet/types": "^0.12.71",
|
||||
"@solana/spl-token": "^0.4.13",
|
||||
"@solana/web3.js": "^1.98.2",
|
||||
"axios": "^1.6.8",
|
||||
"bn.js": "^5.2.2",
|
||||
"next": "15.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Account, Registry, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
import { Registry } from '@cerc-io/registry-sdk';
|
||||
import { GasPrice } from '@cosmjs/stargate';
|
||||
import axios from 'axios';
|
||||
import { verifyUnusedSolanaPayment } from '@/utils/solanaVerify';
|
||||
@ -8,114 +8,6 @@ import { transferLNTTokens } from '@/services/laconicTransfer';
|
||||
// Sleep helper function
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// ATOM payment verification function
|
||||
const verifyAtomPayment = async (txHash: string): Promise<{
|
||||
valid: boolean,
|
||||
reason?: string,
|
||||
amount?: string,
|
||||
sender?: string
|
||||
}> => {
|
||||
try {
|
||||
const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL;
|
||||
const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS;
|
||||
const minPaymentUAtom = '100000'; // 0.1 ATOM in uatom
|
||||
|
||||
if (!apiEndpoint) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'ATOM API endpoint not configured'
|
||||
};
|
||||
}
|
||||
|
||||
if (!recipientAddress) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'ATOM recipient address not configured'
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch transaction from the ATOM API endpoint
|
||||
const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`);
|
||||
|
||||
if (!response.data || !response.data.tx || !response.data.tx_response) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Invalid transaction data from API endpoint'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if transaction was successful
|
||||
const txResponse = response.data.tx_response;
|
||||
if (txResponse.code !== 0) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Transaction failed with code ${txResponse.code}: ${txResponse.raw_log}`
|
||||
};
|
||||
}
|
||||
|
||||
// Check transaction timestamp (5-minute window)
|
||||
const txTimestamp = new Date(txResponse.timestamp);
|
||||
const now = new Date();
|
||||
const timeDiffMs = now.getTime() - txTimestamp.getTime();
|
||||
const timeWindowMs = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
if (timeDiffMs > timeWindowMs) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Transaction is older than 5 minutes (${Math.round(timeDiffMs / 60000)} minutes old)`
|
||||
};
|
||||
}
|
||||
|
||||
// Extract the payment details
|
||||
const tx = response.data.tx;
|
||||
let foundValidPayment = false;
|
||||
let paymentAmountUAtom = '';
|
||||
let sender = '';
|
||||
|
||||
// Get the sender address from the first signer
|
||||
if (tx.auth_info && tx.auth_info.signer_infos && tx.auth_info.signer_infos.length > 0) {
|
||||
sender = tx.auth_info.signer_infos[0].public_key.address || '';
|
||||
}
|
||||
|
||||
// Find the send message in the transaction
|
||||
for (const msg of tx.body.messages) {
|
||||
if (msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') {
|
||||
if (msg.to_address === recipientAddress) {
|
||||
for (const coin of msg.amount) {
|
||||
if (coin.denom === 'uatom') {
|
||||
// Get the amount in uatom
|
||||
paymentAmountUAtom = coin.amount;
|
||||
|
||||
if (parseInt(paymentAmountUAtom) >= parseInt(minPaymentUAtom)) {
|
||||
foundValidPayment = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundValidPayment) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Payment amount (${paymentAmountUAtom || '0'}uatom) is less than required (${minPaymentUAtom}uatom) or not sent to the correct address (${recipientAddress})`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
amount: `${paymentAmountUAtom}uatom`,
|
||||
sender
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error verifying ATOM payment:', error);
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Extract repo name from URL
|
||||
const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, provider: string } => {
|
||||
@ -230,12 +122,11 @@ const registryTransactionWithRetry = async (
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// First check if the request body is valid JSON
|
||||
let url, txHash, paymentType;
|
||||
let url, txHash;
|
||||
try {
|
||||
const body = await request.json();
|
||||
url = body.url;
|
||||
txHash = body.txHash;
|
||||
paymentType = body.paymentType || 'ATOM'; // Default to ATOM for backward compatibility
|
||||
|
||||
if (!url || !txHash) {
|
||||
return NextResponse.json({
|
||||
@ -243,13 +134,6 @@ export async function POST(request: NextRequest) {
|
||||
message: 'Missing required fields: url and txHash are required'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (!['ATOM', 'GOR'].includes(paymentType)) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Invalid payment type. Must be ATOM or GOR'
|
||||
}, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
@ -257,42 +141,24 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify payment based on type
|
||||
if (paymentType === 'ATOM') {
|
||||
console.log('Step 0: Verifying ATOM payment...');
|
||||
const paymentVerificationResult = await verifyAtomPayment(txHash);
|
||||
// Verify Solana payment
|
||||
console.log('Step 0: Verifying Solana token payment...');
|
||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(txHash);
|
||||
|
||||
if (!paymentVerificationResult.valid) {
|
||||
console.error('ATOM payment verification failed:', paymentVerificationResult.reason);
|
||||
if (!solanaPaymentResult.valid) {
|
||||
console.error('Solana token payment verification failed:', solanaPaymentResult.reason);
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: `Payment verification failed: ${paymentVerificationResult.reason}`
|
||||
message: `Payment verification failed: ${solanaPaymentResult.reason}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('ATOM payment verified successfully:', {
|
||||
amount: paymentVerificationResult.amount,
|
||||
sender: paymentVerificationResult.sender
|
||||
console.log('Solana token payment verified successfully:', {
|
||||
amount: solanaPaymentResult.amount,
|
||||
sender: solanaPaymentResult.sender
|
||||
});
|
||||
} else if (paymentType === 'GOR') {
|
||||
console.log('Step 0: Verifying GOR payment...');
|
||||
const gorPaymentResult = await verifyUnusedSolanaPayment(txHash);
|
||||
|
||||
if (!gorPaymentResult.valid) {
|
||||
console.error('GOR payment verification failed:', gorPaymentResult.reason);
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: `Payment verification failed: ${gorPaymentResult.reason}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('GOR payment verified successfully:', {
|
||||
amount: gorPaymentResult.amount,
|
||||
sender: gorPaymentResult.sender
|
||||
});
|
||||
}
|
||||
|
||||
// For both payment types, perform LNT transfer after payment verification
|
||||
// Perform LNT transfer after payment verification
|
||||
console.log('Step 0.5: Performing LNT transfer from prefilled account to service provider...');
|
||||
const lntTransferResult = await transferLNTTokens();
|
||||
|
||||
@ -307,8 +173,8 @@ export async function POST(request: NextRequest) {
|
||||
console.log('LNT transfer completed:', lntTransferResult.transactionHash);
|
||||
const finalTxHash = lntTransferResult.transactionHash!; // Use LNT transfer hash for registry
|
||||
|
||||
// Validate required environment variables based on payment type
|
||||
const baseRequiredEnvVars = [
|
||||
// Validate required environment variables for GOR/Solana payments
|
||||
const requiredEnvVars = [
|
||||
'REGISTRY_CHAIN_ID',
|
||||
'REGISTRY_GQL_ENDPOINT',
|
||||
'REGISTRY_RPC_ENDPOINT',
|
||||
@ -316,29 +182,15 @@ export async function POST(request: NextRequest) {
|
||||
'REGISTRY_AUTHORITY',
|
||||
'REGISTRY_USER_KEY', // This is the same as the prefilled account for LNT transfers
|
||||
'DEPLOYER_LRN',
|
||||
// LNT transfer variables now required for both payment types
|
||||
// LNT transfer variables
|
||||
'LACONIC_SERVICE_PROVIDER_ADDRESS',
|
||||
'LACONIC_TRANSFER_AMOUNT'
|
||||
];
|
||||
|
||||
const atomRequiredEnvVars = [
|
||||
'NEXT_PUBLIC_RECIPIENT_ADDRESS',
|
||||
'NEXT_PUBLIC_COSMOS_API_URL'
|
||||
];
|
||||
|
||||
const gorRequiredEnvVars = [
|
||||
'LACONIC_TRANSFER_AMOUNT',
|
||||
// Solana/GOR specific variables
|
||||
'NEXT_PUBLIC_SOLANA_RPC_URL',
|
||||
'NEXT_PUBLIC_GOR_MINT_ADDRESS',
|
||||
'NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS'
|
||||
'NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS',
|
||||
'NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS'
|
||||
];
|
||||
|
||||
let requiredEnvVars = [...baseRequiredEnvVars];
|
||||
if (paymentType === 'ATOM') {
|
||||
requiredEnvVars = [...requiredEnvVars, ...atomRequiredEnvVars];
|
||||
} else if (paymentType === 'GOR') {
|
||||
requiredEnvVars = [...requiredEnvVars, ...gorRequiredEnvVars];
|
||||
}
|
||||
|
||||
for (const envVar of requiredEnvVars) {
|
||||
if (!process.env[envVar]) {
|
||||
console.error(`Missing environment variable: ${envVar}`);
|
||||
@ -540,9 +392,16 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
note: `Added via ATOM-Deploy @ ${timestamp}`,
|
||||
note: `Added via GOR-Deploy @ ${timestamp}`,
|
||||
repository: repoUrl,
|
||||
repository_ref: fullHash,
|
||||
external_payment: {
|
||||
// Use CAIP convention for chain ID: namespace + reference
|
||||
chain_id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet
|
||||
tx_hash: txHash,
|
||||
// TODO: Take pubkey from user and add it
|
||||
// pubkey: ''
|
||||
}
|
||||
},
|
||||
payment: finalTxHash,
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
|
||||
import "./globals.css";
|
||||
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
|
||||
|
||||
@ -14,8 +15,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Deploy Frontends using ATOM and Laconic",
|
||||
description: "Deploy URLs to Laconic Registry using ATOM payments",
|
||||
title: "Deploy Frontends using GOR and Laconic",
|
||||
description: "Deploy URLs to Laconic Registry using GOR payments",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
101
src/app/page.tsx
101
src/app/page.tsx
@ -1,19 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
// Dynamically import components to avoid SSR issues with browser APIs
|
||||
const KeplrConnect = dynamic(() => import('@/components/KeplrConnect'), { ssr: false });
|
||||
const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false });
|
||||
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import URLForm from '@/components/URLForm';
|
||||
import StatusDisplay from '@/components/StatusDisplay';
|
||||
import { createApplicationDeploymentRequest } from '@/services/registry';
|
||||
import { PaymentType, SolanaWalletState } from '@/types';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { SolanaWalletState } from '@/types';
|
||||
|
||||
// Dynamically import components to avoid SSR issues with browser APIs
|
||||
const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false });
|
||||
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||
|
||||
export default function Home() {
|
||||
const [paymentType, setPaymentType] = useState<PaymentType>('ATOM');
|
||||
const [walletAddress, setWalletAddress] = useState<string | null>(null);
|
||||
const [showWalletConnection, setShowWalletConnection] = useState(false);
|
||||
const [solanaWalletState, setSolanaWalletState] = useState<SolanaWalletState>({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
@ -33,10 +33,6 @@ export default function Home() {
|
||||
const [shortCommitHash, setShortCommitHash] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleConnect = (address: string) => {
|
||||
setWalletAddress(address);
|
||||
};
|
||||
|
||||
const handleSolanaConnect = (walletState: SolanaWalletState) => {
|
||||
setSolanaWalletState(walletState);
|
||||
// Store wallet info globally for PaymentModal access (simplified approach)
|
||||
@ -48,14 +44,8 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaymentTypeChange = (type: PaymentType) => {
|
||||
setPaymentType(type);
|
||||
// Reset wallet states when switching payment types
|
||||
if (type === 'ATOM') {
|
||||
setSolanaWalletState({ connected: false, publicKey: null, walletType: null });
|
||||
} else {
|
||||
setWalletAddress(null);
|
||||
}
|
||||
const handleConnectWallet = () => {
|
||||
setShowWalletConnection(true);
|
||||
};
|
||||
|
||||
const handleUrlSubmit = (submittedUrl: string) => {
|
||||
@ -71,7 +61,7 @@ export default function Home() {
|
||||
try {
|
||||
// Create the Laconic Registry record (payment verification is done in the API)
|
||||
if (url) {
|
||||
const result = await createApplicationDeploymentRequest(url, hash, paymentType);
|
||||
const result = await createApplicationDeploymentRequest(url, hash);
|
||||
|
||||
if (result.status === 'success') {
|
||||
setRecordId(result.id);
|
||||
@ -117,50 +107,34 @@ export default function Home() {
|
||||
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
|
||||
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
||||
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
|
||||
Deploy Frontends with ATOM/GOR + Laconic
|
||||
Deploy Frontends with GOR + Laconic
|
||||
</h1>
|
||||
|
||||
{/* Payment Type Selection */}
|
||||
<div className="mb-6 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
|
||||
Select Payment Method
|
||||
</h2>
|
||||
<div className="flex space-x-4">
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="paymentType"
|
||||
value="ATOM"
|
||||
checked={paymentType === 'ATOM'}
|
||||
onChange={(e) => handlePaymentTypeChange(e.target.value as PaymentType)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span style={{ color: 'var(--foreground)' }}>ATOM (Cosmos)</span>
|
||||
</label>
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="paymentType"
|
||||
value="GOR"
|
||||
checked={paymentType === 'GOR'}
|
||||
onChange={(e) => handlePaymentTypeChange(e.target.value as PaymentType)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span style={{ color: 'var(--foreground)' }}>GOR (Solana)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
|
||||
Connect Your Wallet
|
||||
</h2>
|
||||
{paymentType === 'ATOM' ? (
|
||||
<KeplrConnect onConnect={handleConnect} />
|
||||
{!showWalletConnection ? (
|
||||
<div className="text-center">
|
||||
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Payment method: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
GOR (Solana)
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
onClick={handleConnectWallet}
|
||||
className="px-6 py-2 rounded-lg font-medium transition-colors"
|
||||
style={{
|
||||
background: 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
border: 'none'
|
||||
}}
|
||||
>
|
||||
Connect Solana Wallet
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<SolanaConnect onConnect={handleSolanaConnect} />
|
||||
)}
|
||||
@ -169,16 +143,16 @@ export default function Home() {
|
||||
<div className="mb-8 p-6 rounded-lg" style={{
|
||||
background: 'var(--muted-light)',
|
||||
borderLeft: '4px solid var(--primary)',
|
||||
opacity: (paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) ? '1' : '0.6'
|
||||
opacity: solanaWalletState.connected ? '1' : '0.6'
|
||||
}}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
|
||||
Enter URL to Deploy
|
||||
</h2>
|
||||
<URLForm
|
||||
onSubmit={handleUrlSubmit}
|
||||
disabled={!(paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) || status === 'creating'}
|
||||
disabled={!solanaWalletState.connected || status === 'creating'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -186,7 +160,7 @@ export default function Home() {
|
||||
<div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>4</span>
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
|
||||
Deployment Status
|
||||
</h2>
|
||||
<StatusDisplay
|
||||
@ -206,12 +180,11 @@ export default function Home() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPaymentModal && url && (paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) && (
|
||||
{showPaymentModal && url && solanaWalletState.connected && (
|
||||
<PaymentModal
|
||||
isOpen={showPaymentModal}
|
||||
onClose={handleClosePaymentModal}
|
||||
url={url}
|
||||
paymentType={paymentType}
|
||||
onPaymentComplete={handlePaymentComplete}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,74 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { connectKeplr } from '@/services/keplr';
|
||||
|
||||
interface KeplrConnectProps {
|
||||
onConnect: (address: string) => void;
|
||||
}
|
||||
|
||||
export default function KeplrConnect({ onConnect }: KeplrConnectProps) {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [address, setAddress] = useState<string | null>(null);
|
||||
|
||||
const handleConnect = async () => {
|
||||
setConnecting(true);
|
||||
try {
|
||||
const userAddress = await connectKeplr();
|
||||
if (userAddress) {
|
||||
setAddress(userAddress);
|
||||
onConnect(userAddress);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Keplr:', error);
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check if Keplr is available
|
||||
if (typeof window !== 'undefined' && window.keplr) {
|
||||
// Auto-connect on page load
|
||||
handleConnect();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
||||
{address ? (
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
|
||||
<p className="font-medium" style={{ color: 'var(--success)' }}>Connected</p>
|
||||
</div>
|
||||
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<p className="text-sm font-mono break-all text-center">{address}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={connecting}
|
||||
className="px-6 py-3 rounded-md w-full sm:w-auto transition-colors"
|
||||
style={{
|
||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
opacity: connecting ? '0.8' : '1',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{connecting && (
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{connecting ? 'Connecting...' : 'Connect Keplr Wallet'}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,62 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { sendAtomPayment } from '@/services/keplr';
|
||||
import { sendGorPayment } from '@/services/solana';
|
||||
|
||||
import { sendSolanaTokenPayment } from '@/services/solana';
|
||||
import { PaymentModalProps } from '@/types';
|
||||
import { getSolanaConfig, GOR_PAYMENT_AMOUNT } from '@/config';
|
||||
import { getSolanaConfig } from '@/config';
|
||||
|
||||
export default function PaymentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
url,
|
||||
paymentType,
|
||||
onPaymentComplete,
|
||||
}: PaymentModalProps) {
|
||||
const [amount, setAmount] = useState(paymentType === 'ATOM' ? '0.01' : GOR_PAYMENT_AMOUNT.toString());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Get recipient addresses from environment variables
|
||||
const atomRecipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
|
||||
const gorRecipientAddress = paymentType === 'GOR' ?
|
||||
(process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS || 'solana_recipient_address') : '';
|
||||
|
||||
const recipientAddress = paymentType === 'ATOM' ? atomRecipientAddress : gorRecipientAddress;
|
||||
|
||||
// Validate amount on change
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setAmount(value);
|
||||
|
||||
// Clear error when user types
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
// Get configuration
|
||||
const solanaConfig = getSolanaConfig();
|
||||
const amount = solanaConfig.paymentAmount;
|
||||
const recipientAddress = solanaConfig.tokenRecipientAddress;
|
||||
|
||||
const handlePayment = async () => {
|
||||
// Validate amount before sending
|
||||
const parsedAmount = parseFloat(amount);
|
||||
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
||||
setError('Please enter a valid positive amount');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
if (paymentType === 'ATOM') {
|
||||
const result = await sendAtomPayment(recipientAddress, amount);
|
||||
|
||||
if (result.status === 'success' && result.hash) {
|
||||
onPaymentComplete(result.hash);
|
||||
} else {
|
||||
setError(result.message || 'ATOM payment failed. Please try again.');
|
||||
}
|
||||
} else if (paymentType === 'GOR') {
|
||||
// For GOR payments, we need wallet info from parent component
|
||||
// For Solana payments, we need wallet info from parent component
|
||||
// This is a simplified approach - in a real implementation, you'd pass wallet state
|
||||
const walletInfo = (window as any).solanaWalletInfo;
|
||||
if (!walletInfo || !walletInfo.publicKey || !walletInfo.walletType) {
|
||||
@ -64,14 +33,13 @@ export default function PaymentModal({
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await sendGorPayment(walletInfo.publicKey, walletInfo.walletType);
|
||||
const result = await sendSolanaTokenPayment(walletInfo.publicKey, walletInfo.walletType);
|
||||
|
||||
if (result.success && result.transactionSignature) {
|
||||
onPaymentComplete(result.transactionSignature);
|
||||
} else {
|
||||
setError(result.error || 'GOR payment failed. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
|
||||
} finally {
|
||||
@ -87,7 +55,7 @@ export default function PaymentModal({
|
||||
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
|
||||
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||
Complete {paymentType} Payment
|
||||
Complete GOR Payment
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -108,34 +76,30 @@ export default function PaymentModal({
|
||||
|
||||
<div>
|
||||
<label htmlFor="amount" className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
|
||||
Amount ({paymentType})
|
||||
Amount (GOR)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
min={paymentType === 'ATOM' ? "0.01" : "1"}
|
||||
step={paymentType === 'ATOM' ? "0.01" : "1"}
|
||||
value={amount}
|
||||
onChange={handleAmountChange}
|
||||
disabled={paymentType === 'GOR'} // Fixed amount for GOR
|
||||
disabled={true} // Fixed amount for Solana tokens
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--input-border)',
|
||||
color: 'var(--foreground)',
|
||||
opacity: paymentType === 'GOR' ? '0.7' : '1'
|
||||
opacity: '0.7'
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>{paymentType}</span>
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>GOR</span>
|
||||
</div>
|
||||
</div>
|
||||
{paymentType === 'GOR' && (
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
||||
Fixed amount required for deployment
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@ -174,7 +138,7 @@ export default function PaymentModal({
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{loading ? 'Processing...' : `Pay with ${paymentType === 'ATOM' ? 'Keplr' : 'Solana Wallet'}`}
|
||||
{loading ? 'Processing...' : 'Pay with Solana Wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -125,7 +125,7 @@ export default function StatusDisplay({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{txHash && <InfoItem label="ATOM Payment Transaction Hash" value={txHash} />}
|
||||
{txHash && <InfoItem label="GOR Payment Transaction Hash" value={txHash} />}
|
||||
{appRecordId && <InfoItem label="Laconic Application Record ID" value={appRecordId} />}
|
||||
{recordId && <InfoItem label="Laconic Deployment Request Record ID" value={recordId} />}
|
||||
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
||||
|
@ -40,35 +40,35 @@ export const getDeployerLrn = (): string => {
|
||||
};
|
||||
|
||||
export const getAppName = (): string => {
|
||||
return process.env.APP_NAME || 'atom-deploy';
|
||||
return process.env.APP_NAME || 'gor-deploy';
|
||||
};
|
||||
|
||||
export const COSMOS_DENOM = 'uatom';
|
||||
|
||||
// Solana/GOR Token Configuration
|
||||
export const GOR_PAYMENT_AMOUNT = 50; // 50 GOR tokens
|
||||
export const GOR_TOKEN_DECIMALS = 9; // Standard SPL token decimals
|
||||
|
||||
// Solana Token Configuration (configurable)
|
||||
export const getSolanaConfig = () => {
|
||||
const requiredEnvVars = [
|
||||
'NEXT_PUBLIC_SOLANA_RPC_URL',
|
||||
'NEXT_PUBLIC_GOR_MINT_ADDRESS',
|
||||
'NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS'
|
||||
];
|
||||
// Direct validation without loop to avoid the strange undefined behavior
|
||||
const rpcUrl = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||
const tokenMintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
|
||||
const tokenRecipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
||||
|
||||
for (const envVar of requiredEnvVars) {
|
||||
if (!process.env[envVar]) {
|
||||
throw new Error(`Missing environment variable: ${envVar}`);
|
||||
if (!rpcUrl) {
|
||||
throw new Error(`Missing environment variable: NEXT_PUBLIC_SOLANA_RPC_URL`);
|
||||
}
|
||||
if (!tokenMintAddress) {
|
||||
throw new Error(`Missing environment variable: NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS`);
|
||||
}
|
||||
if (!tokenRecipientAddress) {
|
||||
throw new Error(`Missing environment variable: NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS`);
|
||||
}
|
||||
|
||||
return {
|
||||
rpcUrl: process.env.NEXT_PUBLIC_SOLANA_RPC_URL!,
|
||||
rpcUrl,
|
||||
websocketUrl: process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL,
|
||||
gorMintAddress: process.env.NEXT_PUBLIC_GOR_MINT_ADDRESS!,
|
||||
gorRecipientAddress: process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS!,
|
||||
paymentAmount: GOR_PAYMENT_AMOUNT,
|
||||
tokenDecimals: GOR_TOKEN_DECIMALS
|
||||
tokenMintAddress,
|
||||
tokenRecipientAddress,
|
||||
tokenSymbol: process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL || 'TOKEN',
|
||||
tokenName: process.env.NEXT_PUBLIC_SOLANA_TOKEN_NAME || 'Solana Token',
|
||||
paymentAmount: parseInt(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT || '50'),
|
||||
tokenDecimals: parseInt(process.env.NEXT_PUBLIC_SOLANA_TOKEN_DECIMALS || '9')
|
||||
};
|
||||
};
|
||||
|
||||
@ -92,6 +92,3 @@ export const getLaconicTransferConfig = () => {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,260 +0,0 @@
|
||||
import { SigningStargateClient } from '@cosmjs/stargate';
|
||||
import { TransactionResponse } from '../types';
|
||||
import { COSMOS_DENOM } from '../config';
|
||||
|
||||
export const connectKeplr = async (): Promise<string | null> => {
|
||||
if (!window.keplr) {
|
||||
alert('Keplr wallet extension is not installed!');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const chainId = process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4';
|
||||
|
||||
// Try to suggest chain if custom network
|
||||
if (chainId !== 'cosmoshub-4') {
|
||||
try {
|
||||
// Check if we need to suggest the chain to Keplr
|
||||
await window.keplr.getKey(chainId).catch(async () => {
|
||||
// Chain needs to be suggested
|
||||
if (process.env.NEXT_PUBLIC_COSMOS_RPC_URL) {
|
||||
await window.keplr.experimentalSuggestChain({
|
||||
chainId: chainId,
|
||||
chainName: chainId,
|
||||
rpc: process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
||||
rest: process.env.NEXT_PUBLIC_COSMOS_REST_URL || process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
||||
bip44: {
|
||||
coinType: 118,
|
||||
},
|
||||
bech32Config: {
|
||||
bech32PrefixAccAddr: "cosmos",
|
||||
bech32PrefixAccPub: "cosmospub",
|
||||
bech32PrefixValAddr: "cosmosvaloper",
|
||||
bech32PrefixValPub: "cosmosvaloperpub",
|
||||
bech32PrefixConsAddr: "cosmosvalcons",
|
||||
bech32PrefixConsPub: "cosmosvalconspub",
|
||||
},
|
||||
currencies: [
|
||||
{
|
||||
coinDenom: "ATOM",
|
||||
coinMinimalDenom: "uatom",
|
||||
coinDecimals: 6,
|
||||
},
|
||||
],
|
||||
feeCurrencies: [
|
||||
{
|
||||
coinDenom: "ATOM",
|
||||
coinMinimalDenom: "uatom",
|
||||
coinDecimals: 6,
|
||||
},
|
||||
],
|
||||
stakeCurrency: {
|
||||
coinDenom: "ATOM",
|
||||
coinMinimalDenom: "uatom",
|
||||
coinDecimals: 6,
|
||||
},
|
||||
gasPriceStep: {
|
||||
low: 0.01,
|
||||
average: 0.025,
|
||||
high: 0.04,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (suggestError) {
|
||||
console.warn("Failed to suggest chain to Keplr:", suggestError);
|
||||
// Continue anyway, as enable might still work
|
||||
}
|
||||
}
|
||||
|
||||
// Enable Keplr for the specified chain
|
||||
await window.keplr.enable(chainId);
|
||||
const offlineSigner = window.keplr.getOfflineSigner(chainId);
|
||||
|
||||
// Get the user's account
|
||||
const accounts = await offlineSigner.getAccounts();
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.error('No accounts found in Keplr wallet');
|
||||
return null;
|
||||
}
|
||||
return accounts[0].address;
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Keplr wallet:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const sendAtomPayment = async (
|
||||
recipientAddress: string,
|
||||
amount: string
|
||||
): Promise<TransactionResponse> => {
|
||||
try {
|
||||
if (!window.keplr) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Keplr wallet extension is not installed!'
|
||||
};
|
||||
}
|
||||
|
||||
// Validate recipient address is a valid cosmos address
|
||||
if (!recipientAddress || !recipientAddress.startsWith('cosmos1')) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Invalid recipient address. Must be a valid Cosmos address starting with cosmos1'
|
||||
};
|
||||
}
|
||||
|
||||
// Validate amount is a positive number
|
||||
const parsedAmount = parseFloat(amount);
|
||||
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Invalid amount. Must be a positive number'
|
||||
};
|
||||
}
|
||||
|
||||
// Get the chain ID from environment variables or use default
|
||||
const chainId = process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4';
|
||||
|
||||
// Enable the chain in Keplr, following same logic as connectKeplr
|
||||
if (chainId !== 'cosmoshub-4') {
|
||||
try {
|
||||
// Check if we need to suggest the chain to Keplr
|
||||
await window.keplr.getKey(chainId).catch(async () => {
|
||||
// Chain needs to be suggested
|
||||
if (process.env.NEXT_PUBLIC_COSMOS_RPC_URL) {
|
||||
await window.keplr.experimentalSuggestChain({
|
||||
chainId: chainId,
|
||||
chainName: chainId,
|
||||
rpc: process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
||||
rest: process.env.NEXT_PUBLIC_COSMOS_REST_URL || process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
||||
bip44: { coinType: 118 },
|
||||
bech32Config: {
|
||||
bech32PrefixAccAddr: "cosmos",
|
||||
bech32PrefixAccPub: "cosmospub",
|
||||
bech32PrefixValAddr: "cosmosvaloper",
|
||||
bech32PrefixValPub: "cosmosvaloperpub",
|
||||
bech32PrefixConsAddr: "cosmosvalcons",
|
||||
bech32PrefixConsPub: "cosmosvalconspub",
|
||||
},
|
||||
currencies: [{ coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 }],
|
||||
feeCurrencies: [{ coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 }],
|
||||
stakeCurrency: { coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 },
|
||||
gasPriceStep: { low: 0.01, average: 0.025, high: 0.04 },
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (suggestError) {
|
||||
console.warn("Failed to suggest chain to Keplr:", suggestError);
|
||||
// Continue anyway, as enable might still work
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the chain in Keplr
|
||||
await window.keplr.enable(chainId);
|
||||
const offlineSigner = window.keplr.getOfflineSigner(chainId);
|
||||
|
||||
// Create the Stargate client
|
||||
const rpcEndpoint = process.env.NEXT_PUBLIC_COSMOS_RPC_URL;
|
||||
if (!rpcEndpoint) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'NEXT_PUBLIC_COSMOS_RPC_URL environment variable is not set'
|
||||
};
|
||||
}
|
||||
|
||||
const client = await SigningStargateClient.connectWithSigner(
|
||||
rpcEndpoint,
|
||||
offlineSigner
|
||||
);
|
||||
|
||||
// Get the user's account
|
||||
const accounts = await offlineSigner.getAccounts();
|
||||
if (!accounts || accounts.length === 0) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'No accounts found in Keplr wallet'
|
||||
};
|
||||
}
|
||||
const sender = accounts[0].address;
|
||||
|
||||
// Convert amount to microdenom (e.g., ATOM to uatom)
|
||||
const microAmount = convertToMicroDenom(amount);
|
||||
|
||||
// Send the transaction
|
||||
const result = await client.sendTokens(
|
||||
sender,
|
||||
recipientAddress,
|
||||
[{ denom: COSMOS_DENOM, amount: microAmount }],
|
||||
{
|
||||
amount: [{ denom: COSMOS_DENOM, amount: '5000' }],
|
||||
gas: '200000',
|
||||
}
|
||||
);
|
||||
|
||||
if (!result || !result.transactionHash) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Transaction did not return a valid hash'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hash: result.transactionHash,
|
||||
status: 'success',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to send ATOM payment:', error);
|
||||
// Provide more descriptive error messages for common errors
|
||||
if (error instanceof Error) {
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
if (errorMessage.includes('insufficient funds')) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Insufficient funds in your Keplr wallet to complete this transaction'
|
||||
};
|
||||
} else if (errorMessage.includes('rejected')) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Transaction was rejected in the Keplr wallet'
|
||||
};
|
||||
} else if (errorMessage.includes('timeout')) {
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Transaction timed out. Please try again'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hash: '',
|
||||
status: 'error',
|
||||
message: 'Unknown error occurred while sending payment'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to convert from ATOM to uatom (1 ATOM = 1,000,000 uatom)
|
||||
export const convertToMicroDenom = (amount: string): string => {
|
||||
const parsedAmount = parseFloat(amount);
|
||||
if (isNaN(parsedAmount)) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
return Math.floor(parsedAmount * 1_000_000).toString();
|
||||
};
|
@ -25,7 +25,51 @@ export const transferLNTTokens = async (): Promise<LaconicTransferResult> => {
|
||||
const transferConfig = getLaconicTransferConfig();
|
||||
const registry = getRegistry();
|
||||
|
||||
console.log('Initiating LNT transfer from prefilled account to service provider...');
|
||||
console.log('Resolving deployer LRN to get payment address...');
|
||||
|
||||
// Resolve the deployer LRN to get the payment address
|
||||
const deployerLrn = process.env.DEPLOYER_LRN;
|
||||
if (!deployerLrn) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'DEPLOYER_LRN environment variable is required'
|
||||
};
|
||||
}
|
||||
|
||||
const resolveResult = await registry.resolveNames([deployerLrn]);
|
||||
console.log('Resolve result:', resolveResult);
|
||||
|
||||
if (!resolveResult || resolveResult.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to resolve deployer LRN: ${deployerLrn}`
|
||||
};
|
||||
}
|
||||
|
||||
const deployerRecord = resolveResult[0];
|
||||
if (!deployerRecord.attributes) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Deployer record has no attributes'
|
||||
};
|
||||
}
|
||||
|
||||
// Find the paymentAddress attribute
|
||||
const paymentAddressAttr = deployerRecord.attributes.find(
|
||||
(attr: any) => attr.key === 'paymentAddress'
|
||||
);
|
||||
|
||||
if (!paymentAddressAttr || !paymentAddressAttr.value?.string) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'paymentAddress attribute not found in deployer record'
|
||||
};
|
||||
}
|
||||
|
||||
const paymentAddress = paymentAddressAttr.value.string;
|
||||
console.log('Found payment address:', paymentAddress);
|
||||
|
||||
console.log('Initiating LNT transfer from prefilled account to payment address...');
|
||||
|
||||
// Create fee for transaction
|
||||
const fee = {
|
||||
@ -33,10 +77,10 @@ export const transferLNTTokens = async (): Promise<LaconicTransferResult> => {
|
||||
gas: registryConfig.fee.gas || '900000',
|
||||
};
|
||||
|
||||
// Send tokens from prefilled account to service provider
|
||||
// Send tokens from prefilled account to payment address
|
||||
const transferResult = await registry.sendCoins(
|
||||
{
|
||||
destinationAddress: transferConfig.serviceProviderAddress,
|
||||
destinationAddress: paymentAddress,
|
||||
amount: transferConfig.transferAmount,
|
||||
denom: 'alnt'
|
||||
},
|
||||
|
@ -1,13 +1,11 @@
|
||||
import axios from 'axios';
|
||||
import { CreateRecordResponse, PaymentType } from '../types';
|
||||
import { CreateRecordResponse } from '../types';
|
||||
|
||||
export const createApplicationDeploymentRequest = async (
|
||||
url: string,
|
||||
txHash: string,
|
||||
paymentType: PaymentType = 'ATOM'
|
||||
txHash: string
|
||||
): Promise<CreateRecordResponse> => {
|
||||
try {
|
||||
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${paymentType} payment`);
|
||||
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using GOR payment`);
|
||||
|
||||
// Call our serverless API endpoint to handle the registry interaction
|
||||
const response = await fetch('/api/registry', {
|
||||
@ -15,7 +13,7 @@ export const createApplicationDeploymentRequest = async (
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ url, txHash, paymentType }),
|
||||
body: JSON.stringify({ url, txHash }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
@ -51,63 +49,3 @@ export const createApplicationDeploymentRequest = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyTransaction = async (txHash: string): Promise<boolean> => {
|
||||
try {
|
||||
// Use the public Cosmos API URL for verification queries
|
||||
const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL;
|
||||
if (!apiEndpoint) {
|
||||
console.error('NEXT_PUBLIC_COSMOS_API_URL environment variable not set');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use Axios to directly query the Cosmos transaction via REST API
|
||||
const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`);
|
||||
|
||||
// Check if transaction exists and was successful
|
||||
// The Cosmos API returns a tx_response object with a code field - 0 means success
|
||||
if (response.data &&
|
||||
response.data.tx_response &&
|
||||
response.data.tx_response.code === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check for successful transactions with code === undefined (some nodes report it this way)
|
||||
if (response.data &&
|
||||
response.data.tx_response &&
|
||||
response.data.tx_response.code === undefined &&
|
||||
response.data.tx_response.height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also fallback to checking if the transaction has a height (was included in a block)
|
||||
if (response.data &&
|
||||
response.data.tx_response &&
|
||||
response.data.tx_response.height &&
|
||||
!response.data.tx_response.code) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to verify transaction:', error);
|
||||
|
||||
// If the API call fails, try checking a public explorer API as fallback
|
||||
try {
|
||||
// Try a different URL format that some RPC nodes might use
|
||||
const rpcEndpoint = process.env.NEXT_PUBLIC_COSMOS_RPC_URL;
|
||||
const fallbackResponse = await axios.get(`${rpcEndpoint}/tx?hash=0x${txHash}`);
|
||||
|
||||
if (fallbackResponse.data &&
|
||||
fallbackResponse.data.result &&
|
||||
(fallbackResponse.data.result.height ||
|
||||
(fallbackResponse.data.result.tx_result &&
|
||||
fallbackResponse.data.result.tx_result.code === 0))) {
|
||||
return true;
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback verification also failed:', fallbackError);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
@ -3,10 +3,8 @@ import {
|
||||
TOKEN_PROGRAM_ID,
|
||||
createTransferInstruction,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
getAssociatedTokenAddress
|
||||
} from '@solana/spl-token';
|
||||
import BN from 'bn.js';
|
||||
import { getSolanaConfig } from '../config';
|
||||
import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types';
|
||||
|
||||
@ -79,7 +77,7 @@ export const disconnectSolanaWallet = async (walletType: SolanaWalletType): Prom
|
||||
}
|
||||
};
|
||||
|
||||
export const sendGorPayment = async (
|
||||
export const sendSolanaTokenPayment = async (
|
||||
walletPublicKey: string,
|
||||
walletType: SolanaWalletType
|
||||
): Promise<SolanaPaymentResult> => {
|
||||
@ -102,14 +100,15 @@ export const sendGorPayment = async (
|
||||
|
||||
const connection = getConnection();
|
||||
const senderPublicKey = new PublicKey(walletPublicKey);
|
||||
const mintPublicKey = new PublicKey(config.gorMintAddress);
|
||||
const receiverPublicKey = new PublicKey(config.gorRecipientAddress);
|
||||
const mintPublicKey = new PublicKey(config.tokenMintAddress);
|
||||
const receiverPublicKey = new PublicKey(config.tokenRecipientAddress);
|
||||
|
||||
console.log('Processing GOR payment with keys:', {
|
||||
console.log(`Processing ${config.tokenSymbol} payment with keys:`, {
|
||||
sender: senderPublicKey.toBase58(),
|
||||
mint: mintPublicKey.toBase58(),
|
||||
receiver: receiverPublicKey.toBase58(),
|
||||
amount: config.paymentAmount
|
||||
amount: config.paymentAmount,
|
||||
token: config.tokenSymbol
|
||||
});
|
||||
|
||||
// Get associated token addresses
|
||||
@ -180,7 +179,7 @@ export const sendGorPayment = async (
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||
transaction.feePayer = senderPublicKey;
|
||||
|
||||
console.log('Sending GOR payment transaction...');
|
||||
console.log(`Sending ${config.tokenSymbol} payment transaction...`);
|
||||
const { signature } = await wallet.signAndSendTransaction(transaction);
|
||||
console.log('Transaction sent:', signature);
|
||||
|
||||
@ -201,7 +200,7 @@ export const sendGorPayment = async (
|
||||
transactionSignature: signature
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('GOR payment error:', error);
|
||||
console.error('Solana token payment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Payment failed'
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Window as KeplrWindow } from "@keplr-wallet/types";
|
||||
|
||||
// extend the global Window interface to include Keplr and Solana wallets
|
||||
// extend the global Window interface to include Solana wallets
|
||||
declare global {
|
||||
interface Window extends KeplrWindow {
|
||||
interface Window {
|
||||
phantom?: {
|
||||
solana?: {
|
||||
signAndSendTransaction(transaction: any): Promise<{ signature: string }>;
|
||||
@ -34,27 +32,6 @@ export interface RegistryConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export interface TransactionResponse {
|
||||
hash: string;
|
||||
status: 'success' | 'error';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface LaconicRecordData {
|
||||
type: string;
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
deployer: string;
|
||||
dns: string;
|
||||
meta: {
|
||||
note: string;
|
||||
repository: string;
|
||||
repository_ref: string;
|
||||
};
|
||||
payment: string;
|
||||
}
|
||||
|
||||
export interface CreateRecordResponse {
|
||||
id: string;
|
||||
applicationRecordId?: string;
|
||||
@ -68,9 +45,6 @@ export interface CreateRecordResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Payment types
|
||||
export type PaymentType = 'ATOM' | 'GOR';
|
||||
|
||||
export type SolanaWalletType = 'phantom' | 'solflare';
|
||||
|
||||
export interface SolanaPaymentResult {
|
||||
@ -83,7 +57,6 @@ export interface PaymentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
url: string;
|
||||
paymentType: PaymentType;
|
||||
onPaymentComplete: (txHash: string) => void;
|
||||
}
|
||||
|
||||
|
@ -115,9 +115,8 @@ export const verifySolanaPayment = async (
|
||||
if (parsed.type === 'transferChecked' || parsed.type === 'transfer') {
|
||||
const destination = parsed.info.destination;
|
||||
|
||||
// We need to verify this transfer was to our expected recipient
|
||||
// For now, we'll check if the amount matches and trust the verification
|
||||
if (parsed.info.amount === amount) {
|
||||
// Verify both amount and destination address
|
||||
if (parsed.info.amount === amount && destination === config.tokenRecipientAddress) {
|
||||
foundValidTransfer = true;
|
||||
break;
|
||||
}
|
||||
@ -128,7 +127,7 @@ export const verifySolanaPayment = async (
|
||||
if (!foundValidTransfer) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Valid GOR transfer not found in transaction'
|
||||
reason: 'Valid Solana token transfer not found in transaction'
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user