Add option to accept payments in Gorbagana chain native tokens (#5)
Part of https://www.notion.so/Laconic-Mainnet-Plan-1eca6b22d47280569cd0d1e6d711d949 Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Reviewed-on: #5 Co-authored-by: shreerang <shreerang@noreply.git.vdb.to> Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
This commit is contained in:
parent
b4c6b0aa4e
commit
fbe4eed31d
14
.env.example
14
.env.example
@ -1,14 +1,18 @@
|
|||||||
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
|
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
|
||||||
|
|
||||||
# Solana Token Payment Configuration
|
# Solana Payment Configuration
|
||||||
# TODO: Use different RPC URL or use browser wallet
|
# TODO: Use different RPC URL or use browser wallet
|
||||||
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
|
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
|
||||||
|
|
||||||
# Multisig address
|
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY
|
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
|
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
|
||||||
NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD=5 # Payment amount in USD
|
NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD=5
|
||||||
|
|
||||||
|
# Gorbagana Chain Configuration
|
||||||
|
NEXT_PUBLIC_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf
|
||||||
|
NEXT_PUBLIC_ENABLE_NATIVE_GOR_TRANSFER=true
|
||||||
|
|
||||||
|
# Multisig Address
|
||||||
|
NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY
|
||||||
|
|
||||||
# UI Configuration
|
# UI Configuration
|
||||||
NEXT_PUBLIC_EXAMPLE_URL=https://git.vdb.to/cerc-io/test-progressive-web-app
|
NEXT_PUBLIC_EXAMPLE_URL=https://git.vdb.to/cerc-io/test-progressive-web-app
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
/** @type {import("eslint").Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: ["next/core-web-vitals"],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_"
|
|
||||||
}],
|
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"react-hooks/exhaustive-deps": "warn"
|
|
||||||
}
|
|
||||||
};
|
|
121
CLAUDE.md
121
CLAUDE.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**GOR Deploy** is a Next.js application that bridges Solana blockchain token payments with Laconic Registry deployments. Users pay with GOR tokens (or any configurable Solana SPL token) and the app creates deployment records in the Laconic Registry for frontend applications.
|
**GOR Deploy** is a Next.js application that bridges Solana blockchain token payments with Laconic Registry deployments. Users pay with native GOR tokens or GOR SPL tokens and the app creates deployment records in the Laconic Registry for frontend applications.
|
||||||
|
|
||||||
## Quick Commands
|
## Quick Commands
|
||||||
|
|
||||||
@ -26,12 +26,16 @@ npm run lint # Run ESLint
|
|||||||
- Solana Web3.js for payments
|
- Solana Web3.js for payments
|
||||||
- @cerc-io/registry-sdk for Laconic Registry
|
- @cerc-io/registry-sdk for Laconic Registry
|
||||||
- CosmJS for Cosmos blockchain interactions
|
- CosmJS for Cosmos blockchain interactions
|
||||||
- **Wallet Integration**: Phantom & Solflare
|
- **Wallet Integration**: Phantom, Solflare & Backpack with context-based filtering
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
|
|
||||||
### Key Dependencies
|
### Key Dependencies
|
||||||
- `@solana/web3.js` - Solana blockchain interactions
|
- `@solana/web3.js` - Solana blockchain interactions
|
||||||
- `@solana/spl-token` - SPL token handling
|
- `@solana/spl-token` - SPL token handling
|
||||||
|
- `@solana/wallet-adapter-react` - Unified wallet adapter integration
|
||||||
|
- `@solana/wallet-adapter-phantom` - Phantom wallet adapter
|
||||||
|
- `@solana/wallet-adapter-solflare` - Solflare wallet adapter
|
||||||
|
- `@solana/wallet-adapter-backpack` - Backpack wallet adapter
|
||||||
- `@cerc-io/registry-sdk` - Laconic Registry operations
|
- `@cerc-io/registry-sdk` - Laconic Registry operations
|
||||||
- `@cosmjs/stargate` - Cosmos blockchain operations
|
- `@cosmjs/stargate` - Cosmos blockchain operations
|
||||||
- `axios` - HTTP requests for GitHub API
|
- `axios` - HTTP requests for GitHub API
|
||||||
@ -40,38 +44,58 @@ npm run lint # Run ESLint
|
|||||||
|
|
||||||
## Application Flow
|
## Application Flow
|
||||||
|
|
||||||
1. **Wallet Connection**: User connects Phantom or Solflare wallet
|
1. **Payment Method Selection**: User chooses between native GOR or SPL token payment
|
||||||
2. **URL Input**: User enters repository URL to deploy
|
2. **Wallet Connection**: Compatible wallets are filtered based on payment method
|
||||||
3. **Payment**: Fixed amount payment in GOR/SPL tokens to configured recipient
|
3. **URL Input**: User enters repository URL to deploy (enabled after wallet connection)
|
||||||
4. **Verification**: Server verifies Solana transaction (5-minute window)
|
4. **Payment**: Fixed amount payment in native GOR or GOR SPL tokens to configured recipient
|
||||||
5. **LNT Transfer**: Server transfers LNT tokens for registry fees
|
5. **Verification**: Server verifies Solana transaction (5-minute window)
|
||||||
6. **Registry Records**: Creates ApplicationRecord and ApplicationDeploymentRequest
|
6. **LNT Transfer**: Server transfers LNT tokens for registry fees
|
||||||
7. **Name Mapping**: Sets LRN mappings in Laconic Registry
|
7. **Registry Records**: Creates ApplicationRecord and ApplicationDeploymentRequest
|
||||||
|
8. **Name Mapping**: Sets LRN mappings in Laconic Registry
|
||||||
|
|
||||||
|
## Payment Method & Wallet Compatibility
|
||||||
|
|
||||||
|
### Native GOR Payments (`nat-gor`)
|
||||||
|
- **Amount**: 0.01 GOR (10,000,000 lamports)
|
||||||
|
- **Compatible Wallets**: Backpack only
|
||||||
|
- **RPC Endpoint**: Uses `NEXT_PUBLIC_GORBAGANA_RPC_URL` for native transactions
|
||||||
|
|
||||||
|
### SPL Token Payments (`spl-token`)
|
||||||
|
- **Amount**: $5 USD worth of GOR SPL tokens (dynamic pricing via Jupiter)
|
||||||
|
- **Compatible Wallets**: Phantom, Solflare (Backpack excluded)
|
||||||
|
- **RPC Endpoint**: Uses `NEXT_PUBLIC_SOLANA_RPC_URL` for token transactions
|
||||||
|
|
||||||
## Key Files and Components
|
## Key Files and Components
|
||||||
|
|
||||||
### Frontend Components
|
### Frontend Components
|
||||||
- `src/app/page.tsx` - Main application page with 3-step flow
|
- `src/app/page.tsx` - Main application page with 4-step flow (Payment Method → Wallet → URL → Status)
|
||||||
- `src/components/PaymentModal.tsx` - Solana payment interface
|
- `src/components/PaymentModal.tsx` - Solana payment interface with pre-selected payment method
|
||||||
- `src/components/SolanaConnect.tsx` - Wallet connection UI
|
- `src/components/WalletProviders.tsx` - Context-aware wallet provider with filtering
|
||||||
- `src/components/URLForm.tsx` - URL input form
|
- `src/components/URLForm.tsx` - URL input form
|
||||||
- `src/components/StatusDisplay.tsx` - Deployment status display
|
- `src/components/StatusDisplay.tsx` - Deployment status display
|
||||||
|
|
||||||
|
### Context & State Management
|
||||||
|
- `src/contexts/PaymentMethodContext.tsx` - Global payment method state management
|
||||||
|
- Uses React Context for payment method selection
|
||||||
|
- Wallet filtering based on payment method context
|
||||||
|
|
||||||
### Services and Utils
|
### Services and Utils
|
||||||
- `src/services/solana.ts` - Solana wallet and payment operations
|
- `src/services/solana.ts` - Unified wallet adapter integration for payments
|
||||||
- `src/services/registry.ts` - Client-side registry service wrapper
|
- `src/services/registry.ts` - Client-side registry service wrapper
|
||||||
- `src/services/laconic-transfer.ts` - LNT token transfer logic
|
- `src/services/laconic-transfer.ts` - LNT token transfer logic
|
||||||
- `src/utils/solana-verify.ts` - Payment verification with replay protection
|
- `src/utils/solana-verify.ts` - Payment verification with replay protection
|
||||||
- `src/config/index.ts` - Registry configuration management
|
- `src/config/index.ts` - Registry configuration management
|
||||||
|
- `src/constants/payments.ts` - Payment method types and configuration
|
||||||
|
|
||||||
### API Routes
|
### API Routes
|
||||||
- `src/app/api/registry/route.ts` - Main server-side registry operations
|
- `src/app/api/registry/route.ts` - Main server-side registry operations with dual payment support
|
||||||
|
|
||||||
## Environment Configuration
|
## Environment Configuration
|
||||||
|
|
||||||
### Client-side Variables (NEXT_PUBLIC_*)
|
### Client-side Variables (NEXT_PUBLIC_*)
|
||||||
```bash
|
```bash
|
||||||
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
|
||||||
|
NEXT_PUBLIC_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=<SPL_TOKEN_MINT>
|
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=<SPL_TOKEN_MINT>
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=<RECIPIENT_WALLET>
|
NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS=<RECIPIENT_WALLET>
|
||||||
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
|
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
|
||||||
@ -95,20 +119,29 @@ DEPLOYER_LRN=<DEPLOYER_LRN>
|
|||||||
## Code Patterns and Conventions
|
## Code Patterns and Conventions
|
||||||
|
|
||||||
### State Management
|
### State Management
|
||||||
- Uses React `useState` for component state
|
- Uses React Context (`PaymentMethodContext`) for global payment method state
|
||||||
- No global state management library
|
- React `useState` for component-level state
|
||||||
- Dynamic imports to avoid SSR issues with browser APIs
|
- Dynamic imports to avoid SSR issues with browser APIs
|
||||||
|
- Context-driven wallet filtering
|
||||||
|
|
||||||
|
### Payment Architecture
|
||||||
|
- Unified wallet adapter pattern for all three wallets
|
||||||
|
- Payment method determines wallet availability
|
||||||
|
- Separate RPC endpoints for native vs SPL token transactions
|
||||||
|
- Dynamic pricing via Jupiter API for SPL tokens
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
- Comprehensive try-catch blocks in all async operations
|
- Comprehensive try-catch blocks in all async operations
|
||||||
- User-friendly error messages in UI
|
- User-friendly error messages in UI
|
||||||
- Server-side error logging with detailed context
|
- Server-side error logging with detailed context
|
||||||
|
- Wallet compatibility validation with clear feedback
|
||||||
|
|
||||||
### Security Best Practices
|
### Security Best Practices
|
||||||
- Environment variables for sensitive data
|
- Environment variables for sensitive data
|
||||||
- Transaction verification with replay protection
|
- Transaction verification with replay protection
|
||||||
- Proper SPL token account creation and validation
|
- Proper SPL token account creation and validation
|
||||||
- Private key never exposed to client-side
|
- Private key never exposed to client-side
|
||||||
|
- Wallet signature verification via adapters
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
@ -117,11 +150,18 @@ DEPLOYER_LRN=<DEPLOYER_LRN>
|
|||||||
2. **Services**: Add to `src/services/`
|
2. **Services**: Add to `src/services/`
|
||||||
3. **API Routes**: Add to `src/app/api/`
|
3. **API Routes**: Add to `src/app/api/`
|
||||||
4. **Types**: Update `src/types/index.ts`
|
4. **Types**: Update `src/types/index.ts`
|
||||||
|
5. **Payment Methods**: Update `src/constants/payments.ts`
|
||||||
|
|
||||||
### Payment Integration
|
### Payment Integration
|
||||||
- All payment logic in `src/services/solana.ts`
|
- All payment logic unified in `src/services/solana.ts`
|
||||||
- Verification logic in `src/utils/solana-verify.ts`
|
- Verification logic in `src/utils/solana-verify.ts`
|
||||||
- Transaction signing via wallet adapters
|
- Transaction signing via wallet adapters
|
||||||
|
- Payment method context drives wallet selection
|
||||||
|
|
||||||
|
### Wallet Integration
|
||||||
|
- Add new wallet adapters to `src/components/WalletProviders.tsx`
|
||||||
|
- Update wallet filtering logic based on payment method compatibility
|
||||||
|
- Use `@solana/wallet-adapter-*` packages for consistent integration
|
||||||
|
|
||||||
### Registry Integration
|
### Registry Integration
|
||||||
- Registry operations in `src/app/api/registry/route.ts`
|
- Registry operations in `src/app/api/registry/route.ts`
|
||||||
@ -147,8 +187,9 @@ Format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
|||||||
### Solana Payment Verification
|
### Solana Payment Verification
|
||||||
- 5-minute transaction window
|
- 5-minute transaction window
|
||||||
- Amount verification against configured payment
|
- Amount verification against configured payment
|
||||||
- Recipient address validation
|
- Recipient address validation (different for native vs SPL)
|
||||||
- Replay attack protection (TODO: implement used transaction tracking)
|
- Replay attack protection (TODO: implement used transaction tracking)
|
||||||
|
- Payment method-specific validation logic
|
||||||
|
|
||||||
### Laconic Registry Operations
|
### Laconic Registry Operations
|
||||||
1. **ApplicationRecord**: Repository metadata and commit info
|
1. **ApplicationRecord**: Repository metadata and commit info
|
||||||
@ -158,20 +199,45 @@ Format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
|||||||
## Common Development Tasks
|
## Common Development Tasks
|
||||||
|
|
||||||
### Adding New Wallet Support
|
### Adding New Wallet Support
|
||||||
1. Update `SolanaWalletType` in `src/types/index.ts`
|
1. Install wallet adapter package: `npm install @solana/wallet-adapter-{wallet-name}`
|
||||||
2. Add wallet logic to `src/services/solana.ts`
|
2. Add to `src/components/WalletProviders.tsx` wallets array
|
||||||
3. Update `SolanaConnect.tsx` component
|
3. Update wallet filtering logic based on payment method compatibility
|
||||||
|
4. Test with both payment methods
|
||||||
|
|
||||||
|
### Adding New Payment Methods
|
||||||
|
1. Update `PaymentMethod` type in `src/constants/payments.ts`
|
||||||
|
2. Add payment method label to `PAYMENT_METHOD_LABELS`
|
||||||
|
3. Update wallet filtering logic in `WalletProviders.tsx`
|
||||||
|
4. Add payment processing logic in `src/services/solana.ts`
|
||||||
|
5. Update verification logic in `src/utils/solana-verify.ts`
|
||||||
|
|
||||||
### Modifying Payment Verification
|
### Modifying Payment Verification
|
||||||
- Update logic in `src/utils/solana-verify.ts`
|
- Update logic in `src/utils/solana-verify.ts`
|
||||||
- Consider replay protection requirements
|
- Consider replay protection requirements
|
||||||
- Test with various transaction types
|
- Test with various transaction types
|
||||||
|
- Handle both native GOR and SPL token verification
|
||||||
|
|
||||||
### Registry Schema Changes
|
### Registry Schema Changes
|
||||||
- Update record structures in `src/app/api/registry/route.ts`
|
- Update record structures in `src/app/api/registry/route.ts`
|
||||||
- Ensure compatibility with Laconic Registry schema
|
- Ensure compatibility with Laconic Registry schema
|
||||||
- Test with registry validation
|
- Test with registry validation
|
||||||
|
|
||||||
|
## Payment Method Context Usage
|
||||||
|
|
||||||
|
The `PaymentMethodContext` drives wallet filtering:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In components
|
||||||
|
const { selectedPaymentMethod, setSelectedPaymentMethod } = usePaymentMethod();
|
||||||
|
|
||||||
|
// Wallet filtering logic
|
||||||
|
if (selectedPaymentMethod === 'nat-gor') {
|
||||||
|
// Show only Backpack wallet
|
||||||
|
} else if (selectedPaymentMethod === 'spl-token') {
|
||||||
|
// Show only Phantom and Solflare wallets
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Deployment Considerations
|
## Deployment Considerations
|
||||||
|
|
||||||
### Production Setup
|
### Production Setup
|
||||||
@ -179,6 +245,7 @@ Format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
|||||||
- HTTPS required for wallet connections
|
- HTTPS required for wallet connections
|
||||||
- Consider rate limiting for API routes
|
- Consider rate limiting for API routes
|
||||||
- Monitor gas fees and LNT transfer costs
|
- Monitor gas fees and LNT transfer costs
|
||||||
|
- Ensure both RPC endpoints are reliable
|
||||||
|
|
||||||
### Docker Deployment
|
### Docker Deployment
|
||||||
- Dockerfile provided in project root
|
- Dockerfile provided in project root
|
||||||
@ -189,15 +256,18 @@ Format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
|||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
- **Wallet connection fails**: Check HTTPS and wallet extension
|
- **Wallet connection fails**: Check HTTPS and wallet extension
|
||||||
- **Transaction verification fails**: Check RPC URL and transaction timing
|
- **Wrong wallet shown**: Verify payment method context is set correctly
|
||||||
|
- **Transaction verification fails**: Check appropriate RPC URL and transaction timing
|
||||||
- **Registry errors**: Verify all REGISTRY_* environment variables
|
- **Registry errors**: Verify all REGISTRY_* environment variables
|
||||||
- **LNT transfer fails**: Check private key balance and permissions
|
- **LNT transfer fails**: Check private key balance and permissions
|
||||||
|
- **SPL token price errors**: Verify Jupiter API connectivity
|
||||||
|
|
||||||
### Debug Tools
|
### Debug Tools
|
||||||
- Browser console for client-side debugging
|
- Browser console for client-side debugging
|
||||||
- Server logs for API route debugging
|
- Server logs for API route debugging
|
||||||
- Solana Explorer for transaction verification
|
- Solana Explorer for transaction verification
|
||||||
- Registry CLI for direct registry interactions
|
- Registry CLI for direct registry interactions
|
||||||
|
- Payment method context state in React DevTools
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
@ -205,6 +275,8 @@ Format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
|||||||
- Use testnet/devnet for development
|
- Use testnet/devnet for development
|
||||||
- Test transaction verification edge cases
|
- Test transaction verification edge cases
|
||||||
- Verify registry record creation and name mappings
|
- Verify registry record creation and name mappings
|
||||||
|
- Test wallet filtering with different payment methods
|
||||||
|
- Test both native GOR and SPL token payment flows
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
|
|
||||||
@ -219,7 +291,8 @@ This application references:
|
|||||||
- Transaction replay protection implemented
|
- Transaction replay protection implemented
|
||||||
- Input validation on all user inputs
|
- Input validation on all user inputs
|
||||||
- Secure RPC endpoint usage
|
- Secure RPC endpoint usage
|
||||||
- Wallet signature verification
|
- Wallet signature verification via adapters
|
||||||
|
- Payment method validation before processing
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
@ -228,3 +301,5 @@ This application references:
|
|||||||
- Enhanced error recovery and retry logic
|
- Enhanced error recovery and retry logic
|
||||||
- Monitoring and analytics integration
|
- Monitoring and analytics integration
|
||||||
- Multi-chain payment support
|
- Multi-chain payment support
|
||||||
|
- Additional wallet adapter integrations
|
||||||
|
- Advanced payment method configurations
|
@ -11,6 +11,19 @@ const compat = new FlatCompat({
|
|||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}],
|
||||||
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||||
|
"react/jsx-indent": ["error", 2],
|
||||||
|
"react/jsx-indent-props": ["error", 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
4693
package-lock.json
generated
4693
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -12,9 +12,14 @@
|
|||||||
"@cerc-io/registry-sdk": "^0.2.11",
|
"@cerc-io/registry-sdk": "^0.2.11",
|
||||||
"@cosmjs/stargate": "^0.32.3",
|
"@cosmjs/stargate": "^0.32.3",
|
||||||
"@solana/spl-token": "^0.4.13",
|
"@solana/spl-token": "^0.4.13",
|
||||||
|
"@solana/wallet-adapter-backpack": "^0.1.9",
|
||||||
|
"@solana/wallet-adapter-base": "^0.9.18",
|
||||||
|
"@solana/wallet-adapter-phantom": "^0.9.28",
|
||||||
|
"@solana/wallet-adapter-react": "^0.15.20",
|
||||||
|
"@solana/wallet-adapter-react-ui": "^0.9.18",
|
||||||
|
"@solana/wallet-adapter-solflare": "^0.6.32",
|
||||||
"@solana/web3.js": "^1.98.2",
|
"@solana/web3.js": "^1.98.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"big.js": "^6.2.2",
|
|
||||||
"bn.js": "^5.2.2",
|
"bn.js": "^5.2.2",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@ -26,7 +31,9 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||||
|
"@typescript-eslint/parser": "^8.38.0",
|
||||||
|
"eslint": "^9.31.0",
|
||||||
"eslint-config-next": "15.3.1",
|
"eslint-config-next": "15.3.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
@ -10,10 +10,15 @@ import { DENOM as ALNT_DENOM } from '@cerc-io/registry-sdk';
|
|||||||
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
||||||
import { transferLNTTokens } from '@/services/laconic-transfer';
|
import { transferLNTTokens } from '@/services/laconic-transfer';
|
||||||
import { getRegistry, getRegistryConfig } from '@/config';
|
import { getRegistry, getRegistryConfig } from '@/config';
|
||||||
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||||
|
import { IS_NAT_GOR_TRANSFER_ENABLED, SOLANA_GOR_MINT_ADDRESS } from '@/constants/payments';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||||
|
assert(!IS_NAT_GOR_TRANSFER_ENABLED || process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required when NAT GOR transfer is enabled');
|
||||||
|
|
||||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||||
|
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
||||||
|
|
||||||
// Allow 20% slippage due to price fluctuations
|
// Allow 20% slippage due to price fluctuations
|
||||||
const ALLOWED_SLIPPAGE_FACTOR = 0.2
|
const ALLOWED_SLIPPAGE_FACTOR = 0.2
|
||||||
@ -134,21 +139,43 @@ export const registryTransactionWithRetry = async (
|
|||||||
throw lastError;
|
throw lastError;
|
||||||
};
|
};
|
||||||
|
|
||||||
let connection: Connection;
|
let solanaConnection: Connection | null = null;
|
||||||
|
let gorbaganaConnection: Connection | null = null;
|
||||||
|
|
||||||
|
// Helper function to get the appropriate connection based on payment method
|
||||||
|
const getConnection = (paymentMethod: PaymentMethod): Connection => {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentMethod.NAT_GOR:
|
||||||
|
if (!gorbaganaConnection) {
|
||||||
|
gorbaganaConnection = new Connection(GORBAGANA_RPC_URL!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gorbaganaConnection;
|
||||||
|
case PaymentMethod.SPL_TOKEN:
|
||||||
|
if (!solanaConnection) {
|
||||||
|
solanaConnection = new Connection(SOLANA_RPC_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return solanaConnection;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid payment method");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
if (!connection) {
|
|
||||||
connection = new Connection(SOLANA_RPC_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check if the request body is valid JSON
|
// First check if the request body is valid JSON
|
||||||
let url, txHash, senderPublicKey;
|
let url, txHash, senderPublicKey, paymentMethod;
|
||||||
|
let connection: Connection;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
url = body.url;
|
url = body.url;
|
||||||
txHash = body.txHash;
|
txHash = body.txHash;
|
||||||
|
paymentMethod = body.paymentMethod as PaymentMethod;
|
||||||
|
|
||||||
|
// Get the appropriate connection based on payment method
|
||||||
|
connection = getConnection(paymentMethod);
|
||||||
|
|
||||||
const tx = await connection.getParsedTransaction(txHash, 'confirmed');
|
const tx = await connection.getParsedTransaction(txHash, 'confirmed');
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
@ -165,10 +192,18 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
senderPublicKey = signerKeys[0];
|
senderPublicKey = signerKeys[0];
|
||||||
|
|
||||||
if (!url || !txHash) {
|
if (!url || !txHash || !paymentMethod) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Missing required fields: url and txHash are required'
|
message: 'Missing required fields: url, txHash, and paymentMethod are required'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate payment method
|
||||||
|
if (paymentMethod !== PaymentMethod.NAT_GOR && paymentMethod !== PaymentMethod.SPL_TOKEN) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Invalid payment method. Must be "nat-gor" or "spl-token".'
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -180,36 +215,54 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify Solana payment
|
// Verify Solana payment based on method
|
||||||
console.log('Step 0: Verifying Solana token payment...');
|
console.log(`Step 0: Verifying Solana ${paymentMethod} payment...`);
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate expected token amount based on current price
|
||||||
|
let requiredTokenInfo: RequiredTokenInfo;
|
||||||
|
|
||||||
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
||||||
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||||
|
|
||||||
// Calculate expected token amount based on current price
|
|
||||||
let expectedTokenAmount: number;
|
|
||||||
try {
|
try {
|
||||||
const { requiredAmountInBaseUnits } = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, SOLANA_GOR_MINT_ADDRESS);
|
||||||
|
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Unsupported payment method'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calculating token amount:', error);
|
console.error('Error calculating expected amount:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Unable to verify payment due to price calculation error'
|
message: 'Unable to verify payment due to price calculation error'
|
||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenAmount = new BN(expectedTokenAmount);
|
const requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
|
||||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(connection, txHash, tokenAmount);
|
const expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
||||||
|
|
||||||
|
const solanaPaymentResult = await verifyUnusedSolanaPayment(
|
||||||
|
connection,
|
||||||
|
txHash,
|
||||||
|
new BN(expectedTokenAmount),
|
||||||
|
paymentMethod,
|
||||||
|
);
|
||||||
|
|
||||||
if (!solanaPaymentResult.valid) {
|
if (!solanaPaymentResult.valid) {
|
||||||
console.error('Solana token payment verification failed:', solanaPaymentResult.reason);
|
console.error(`Solana ${paymentMethod} payment verification failed:`, solanaPaymentResult.reason);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `Payment verification failed: ${solanaPaymentResult.reason}`
|
message: `Payment verification failed: ${solanaPaymentResult.reason}`
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Solana token payment verified successfully:', {
|
console.log(`Solana ${paymentMethod} payment verified successfully:`, {
|
||||||
amount: solanaPaymentResult.amount,
|
amount: solanaPaymentResult.amount,
|
||||||
sender: solanaPaymentResult.sender
|
sender: solanaPaymentResult.sender
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,9 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
|
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
|
||||||
|
import WalletProviders from "../components/WalletProviders";
|
||||||
|
import { PaymentMethodProvider } from "../contexts/PaymentMethodContext";
|
||||||
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -30,7 +33,11 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<ErrorBoundaryWrapper />
|
<ErrorBoundaryWrapper />
|
||||||
|
<PaymentMethodProvider>
|
||||||
|
<WalletProviders>
|
||||||
{children}
|
{children}
|
||||||
|
</WalletProviders>
|
||||||
|
</PaymentMethodProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
234
src/app/page.tsx
234
src/app/page.tsx
@ -1,24 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
||||||
|
import { useWallet } from '@solana/wallet-adapter-react';
|
||||||
|
import { BackpackWalletName } from '@solana/wallet-adapter-backpack';
|
||||||
|
|
||||||
import URLForm from '@/components/URLForm';
|
import URLForm from '@/components/URLForm';
|
||||||
import StatusDisplay from '@/components/StatusDisplay';
|
import StatusDisplay from '@/components/StatusDisplay';
|
||||||
import { createApplicationDeploymentRequest } from '@/services/registry';
|
import { createApplicationDeploymentRequest } from '@/services/registry';
|
||||||
import { SolanaWalletState } from '@/types';
|
import { IS_NAT_GOR_TRANSFER_ENABLED, PAYMENT_METHOD_LABELS } from '@/constants/payments';
|
||||||
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
// Dynamically import components to avoid SSR issues with browser APIs
|
// 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 });
|
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||||
|
|
||||||
|
const GORBAGANA_GENESIS_HASH = '533uBE9RRquhTBqEX58oV52FdTTsReMdAvaUvP6hNjsn';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [showWalletConnection, setShowWalletConnection] = useState(false);
|
const { wallet, connected, publicKey, disconnect } = useWallet();
|
||||||
const [solanaWalletState, setSolanaWalletState] = useState<SolanaWalletState>({
|
|
||||||
connected: false,
|
const { selectedPaymentMethod, setSelectedPaymentMethod } = usePaymentMethod();
|
||||||
publicKey: null,
|
|
||||||
walletType: null
|
|
||||||
});
|
|
||||||
const [url, setUrl] = useState<string | null>(null);
|
const [url, setUrl] = useState<string | null>(null);
|
||||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||||
const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle');
|
const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle');
|
||||||
@ -29,21 +33,70 @@ export default function Home() {
|
|||||||
const [dns, setDns] = useState<string | null>(null);
|
const [dns, setDns] = useState<string | null>(null);
|
||||||
const [appName, setAppName] = useState<string | null>(null);
|
const [appName, setAppName] = useState<string | null>(null);
|
||||||
const [repoUrl, setRepoUrl] = useState<string | null>(null);
|
const [repoUrl, setRepoUrl] = useState<string | null>(null);
|
||||||
const [commitHash, setCommitHash] = useState<string | null>(null);
|
|
||||||
const [shortCommitHash, setShortCommitHash] = useState<string | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [incorrectChainWarining, setIncorrectChainWarining] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleConnectWallet = () => {
|
useEffect(() => {
|
||||||
setShowWalletConnection(true);
|
if (!IS_NAT_GOR_TRANSFER_ENABLED) {
|
||||||
};
|
setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN);
|
||||||
|
}
|
||||||
|
}, [setSelectedPaymentMethod]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wallet || wallet.adapter.name !== BackpackWalletName || selectedPaymentMethod !== PaymentMethod.NAT_GOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnOnIncorrectChain = async () => {
|
||||||
|
// @ts-expect-error: backpack exists on window object
|
||||||
|
const genesisHash = await window.backpack.solana.connection.getGenesisHash();
|
||||||
|
if (genesisHash !== GORBAGANA_GENESIS_HASH) {
|
||||||
|
setIncorrectChainWarining("WARNING: Unsupported chain selected in wallet. Please switch to Gorbagana chain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warnOnIncorrectChain();
|
||||||
|
}, [wallet, selectedPaymentMethod]);
|
||||||
|
|
||||||
|
// Track previous payment method to detect switches
|
||||||
|
const previousPaymentMethodRef = useRef<PaymentMethod | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPaymentMethod === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only disconnect if switching between different payment methods while connected
|
||||||
|
if (previousPaymentMethodRef.current !== null &&
|
||||||
|
previousPaymentMethodRef.current !== selectedPaymentMethod &&
|
||||||
|
connected) {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPaymentMethodRef.current = selectedPaymentMethod;
|
||||||
|
}, [selectedPaymentMethod, connected, disconnect]);
|
||||||
|
|
||||||
const handleUrlSubmit = (submittedUrl: string) => {
|
const handleUrlSubmit = (submittedUrl: string) => {
|
||||||
setUrl(submittedUrl);
|
setUrl(submittedUrl);
|
||||||
setShowPaymentModal(true);
|
setShowPaymentModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaymentComplete = useCallback(async (hash: string) => {
|
// Helper function to check if current wallet is compatible with selected payment method
|
||||||
if (!solanaWalletState.publicKey || !url) {
|
const isWalletCompatible = () => {
|
||||||
|
if (!selectedPaymentMethod || !wallet) return false;
|
||||||
|
|
||||||
|
const walletName = wallet.adapter.name.toLowerCase();
|
||||||
|
const isBackpack = walletName.includes('backpack');
|
||||||
|
|
||||||
|
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
|
return isBackpack; // Only Backpack for native GOR
|
||||||
|
} else {
|
||||||
|
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
|
||||||
|
if (!publicKey || !url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +106,7 @@ export default function Home() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create the Laconic Registry record (payment verification is done in the API)
|
// Create the Laconic Registry record (payment verification is done in the API)
|
||||||
const result = await createApplicationDeploymentRequest(url, hash);
|
const result = await createApplicationDeploymentRequest(url, hash, paymentMethod);
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
setRecordId(result.id);
|
setRecordId(result.id);
|
||||||
@ -72,12 +125,6 @@ export default function Home() {
|
|||||||
if (result.repoUrl) {
|
if (result.repoUrl) {
|
||||||
setRepoUrl(result.repoUrl);
|
setRepoUrl(result.repoUrl);
|
||||||
}
|
}
|
||||||
if (result.commitHash) {
|
|
||||||
setCommitHash(result.commitHash);
|
|
||||||
}
|
|
||||||
if (result.shortCommitHash) {
|
|
||||||
setShortCommitHash(result.shortCommitHash);
|
|
||||||
}
|
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
} else {
|
} else {
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
@ -87,7 +134,7 @@ export default function Home() {
|
|||||||
setStatus('error');
|
setStatus('error');
|
||||||
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
||||||
}
|
}
|
||||||
}, [solanaWalletState, url])
|
}, [publicKey, url])
|
||||||
|
|
||||||
const handleClosePaymentModal = () => {
|
const handleClosePaymentModal = () => {
|
||||||
setShowPaymentModal(false);
|
setShowPaymentModal(false);
|
||||||
@ -101,56 +148,144 @@ export default function Home() {
|
|||||||
Deploy Frontends with {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + Laconic
|
Deploy Frontends with {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + Laconic
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{/* Step 1: Payment Method Selection */}
|
||||||
|
{ IS_NAT_GOR_TRANSFER_ENABLED &&
|
||||||
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
<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">
|
<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"
|
Choose Payment Method
|
||||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
|
|
||||||
Connect Your Wallet
|
|
||||||
</h2>
|
</h2>
|
||||||
{!showWalletConnection ? (
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="text-center">
|
|
||||||
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
|
|
||||||
Payment method: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
|
|
||||||
{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana)
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
onClick={handleConnectWallet}
|
onClick={() => setSelectedPaymentMethod(PaymentMethod.NAT_GOR)}
|
||||||
className="px-6 py-2 rounded-lg font-medium transition-colors"
|
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||||
|
selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--primary)',
|
backgroundColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--accent)' : 'var(--card-bg)',
|
||||||
color: 'var(--primary-foreground)',
|
borderColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--primary)' : 'var(--card-border)'
|
||||||
border: 'none'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Connect Solana Wallet
|
<div className="text-left">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">GOR (native)</h3>
|
||||||
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
|
Compatible with: Backpack
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN)}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||||
|
selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--accent)' : 'var(--card-bg)',
|
||||||
|
borderColor: selectedPaymentMethod === PaymentMethod.SPL_TOKEN ? 'var(--primary)' : 'var(--card-border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-left">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Token</h3>
|
||||||
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
|
Compatible with: Phantom, Solflare
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Step 2: Wallet Connection */}
|
||||||
|
<div className="mb-10 p-6 rounded-lg" style={{
|
||||||
|
background: 'var(--muted-light)',
|
||||||
|
borderLeft: '4px solid var(--primary)',
|
||||||
|
opacity: selectedPaymentMethod ? '1' : '0.6'
|
||||||
|
}}>
|
||||||
|
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||||
|
Connect Compatible Wallet
|
||||||
|
</h2>
|
||||||
|
{!selectedPaymentMethod ? (
|
||||||
|
<p className="text-center" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
|
Please select a payment method first
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<SolanaConnect onConnect={(walletState) => setSolanaWalletState(walletState)} />
|
<div className="text-center">
|
||||||
|
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
|
Selected: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||||
|
{PAYMENT_METHOD_LABELS[selectedPaymentMethod]}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{connected && publicKey ? (
|
||||||
|
<div className="flex flex-col items-center space-y-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="w-3 h-3 rounded-full mr-2" style={{
|
||||||
|
backgroundColor: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
|
||||||
|
}}></span>
|
||||||
|
<p className="font-medium" style={{
|
||||||
|
color: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
|
||||||
|
}}>
|
||||||
|
{isWalletCompatible() ? 'Compatible' : 'Incompatible'} Wallet ({wallet?.adapter.name})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!isWalletCompatible() && (
|
||||||
|
<p className="text-sm text-amber-400">
|
||||||
|
This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments.
|
||||||
|
Please select a different wallet or payment method.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{incorrectChainWarining && (
|
||||||
|
<p className="text-sm text-amber-400">
|
||||||
|
{incorrectChainWarining}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<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">{publicKey.toBase58()}</p>
|
||||||
|
</div>
|
||||||
|
<WalletMultiButton
|
||||||
|
className="!px-4 !py-2 !rounded-md !text-sm !transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--muted)',
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
border: '1px solid var(--input-border)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<p className="mb-4 text-sm" style={{ color: 'var(--muted-foreground)' }}>
|
||||||
|
{selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||||
|
? 'Only Backpack wallet supports native GOR payments'
|
||||||
|
: 'Phantom and Solflare wallets support SPL token payments'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<WalletMultiButton
|
||||||
|
className="!px-6 !py-3 !rounded-md !w-full !transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--primary)',
|
||||||
|
color: 'var(--primary-foreground)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Step 3: URL Input */}
|
||||||
<div className="mb-8 p-6 rounded-lg" style={{
|
<div className="mb-8 p-6 rounded-lg" style={{
|
||||||
background: 'var(--muted-light)',
|
background: 'var(--muted-light)',
|
||||||
borderLeft: '4px solid var(--primary)',
|
borderLeft: '4px solid var(--primary)',
|
||||||
opacity: solanaWalletState.connected ? '1' : '0.6'
|
opacity: (connected && isWalletCompatible()) ? '1' : '0.6'
|
||||||
}}>
|
}}>
|
||||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
<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>
|
|
||||||
Enter URL to Deploy
|
Enter URL to Deploy
|
||||||
</h2>
|
</h2>
|
||||||
<URLForm
|
<URLForm
|
||||||
onSubmit={handleUrlSubmit}
|
onSubmit={handleUrlSubmit}
|
||||||
disabled={!solanaWalletState.connected || status === 'creating'}
|
disabled={!connected || !isWalletCompatible() || status === 'creating'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status !== 'idle' && (
|
{status !== 'idle' && (
|
||||||
<div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
<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">
|
<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>
|
|
||||||
Deployment Status
|
Deployment Status
|
||||||
</h2>
|
</h2>
|
||||||
<StatusDisplay
|
<StatusDisplay
|
||||||
@ -162,21 +297,18 @@ export default function Home() {
|
|||||||
dns={dns || undefined}
|
dns={dns || undefined}
|
||||||
appName={appName || undefined}
|
appName={appName || undefined}
|
||||||
repoUrl={repoUrl || undefined}
|
repoUrl={repoUrl || undefined}
|
||||||
commitHash={commitHash || undefined}
|
|
||||||
shortCommitHash={shortCommitHash || undefined}
|
|
||||||
error={error || undefined}
|
error={error || undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPaymentModal && url && solanaWalletState.connected && (
|
{showPaymentModal && url && connected && publicKey && selectedPaymentMethod && (
|
||||||
<PaymentModal
|
<PaymentModal
|
||||||
isOpen={showPaymentModal}
|
isOpen={showPaymentModal}
|
||||||
onClose={handleClosePaymentModal}
|
onClose={handleClosePaymentModal}
|
||||||
url={url}
|
url={url}
|
||||||
onPaymentComplete={handlePaymentComplete}
|
onPaymentComplete={handlePaymentComplete}
|
||||||
walletState={solanaWalletState}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,64 +1,101 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo, useState, useEffect } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import BN from 'bn.js';
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
import { Connection } from '@solana/web3.js';
|
import { Connection } from '@solana/web3.js';
|
||||||
|
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
|
||||||
|
|
||||||
import { sendSolanaTokenPayment } from '@/services/solana';
|
import { sendSolanaPayment } from '@/services/solana';
|
||||||
import { getRequiredTokenInfo } from '@/services/jupiter-price';
|
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||||
import { PaymentModalProps } from '@/types';
|
import { PaymentMethod, PaymentModalProps, PaymentRequest } from '@/types';
|
||||||
|
import { IS_NAT_GOR_TRANSFER_ENABLED, PAYMENT_METHOD_LABELS, SOLANA_GOR_MINT_ADDRESS } from '@/constants/payments';
|
||||||
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
assert(!IS_NAT_GOR_TRANSFER_ENABLED || process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL, 'GORBAGANA_RPC_URL is required when NAT GOR transfer is enabled');
|
||||||
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
|
||||||
|
const GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
||||||
|
|
||||||
export default function PaymentModal({
|
export default function PaymentModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
url,
|
url,
|
||||||
onPaymentComplete,
|
onPaymentComplete,
|
||||||
walletState,
|
|
||||||
}: PaymentModalProps) {
|
}: PaymentModalProps) {
|
||||||
|
const { selectedPaymentMethod: paymentMethod } = usePaymentMethod();
|
||||||
|
|
||||||
|
const { connection: solanaConnection } = useConnection();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [tokenAmount, setTokenAmount] = useState<number>(0);
|
const [tokenAmount, setTokenAmount] = useState<number>(0);
|
||||||
const [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback
|
const [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback
|
||||||
const [loadingPrice, setLoadingPrice] = useState(true);
|
const [loadingPrice, setLoadingPrice] = useState(false);
|
||||||
|
|
||||||
const connection = useMemo(() => new Connection(SOLANA_RPC_URL), [])
|
const { wallet, publicKey } = useWallet();
|
||||||
|
|
||||||
// Get configuration from environment variables
|
// Get configuration from environment variables
|
||||||
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
|
||||||
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||||
const recipientAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
const tokenSymbol = process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL;
|
||||||
|
|
||||||
// Fetch token amount based on USD price
|
// Fetch payment amount based on USD price for both payment methods
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen || !paymentMethod) {
|
||||||
|
setLoadingPrice(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchTokenAmount = async () => {
|
const fetchPaymentAmount = async () => {
|
||||||
setLoadingPrice(true);
|
setLoadingPrice(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {requiredAmountInBaseUnits, decimals} = await getRequiredTokenInfo(targetUsdAmount, mintAddress)
|
let requiredTokenInfo: RequiredTokenInfo
|
||||||
setTokenAmount(requiredAmountInBaseUnits);
|
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
setTokenDecimals(decimals);
|
// Fetch native GOR amount using solana GOR token price
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, SOLANA_GOR_MINT_ADDRESS);
|
||||||
|
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||||
|
// Fetch SPL token amount using token mint price
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||||
|
} else {
|
||||||
|
setError('Invalid payment method');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTokenAmount(requiredTokenInfo.requiredAmountInBaseUnits);
|
||||||
|
setTokenDecimals(requiredTokenInfo.decimals);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching token price:', error);
|
console.error('Error fetching payment amount:', error);
|
||||||
setError('Unable to fetch current token price. Please try again.');
|
setError('Unable to fetch current payment amount. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingPrice(false);
|
setLoadingPrice(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTokenAmount();
|
fetchPaymentAmount();
|
||||||
}, [isOpen, targetUsdAmount, mintAddress]);
|
}, [isOpen, paymentMethod, targetUsdAmount, mintAddress]);
|
||||||
|
|
||||||
|
// Initialize state when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setError('');
|
||||||
|
setTokenAmount(0);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const handlePayment = useCallback(async () => {
|
const handlePayment = useCallback(async () => {
|
||||||
|
if (!paymentMethod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tokenAmount === 0 || loadingPrice) {
|
if (tokenAmount === 0 || loadingPrice) {
|
||||||
setError('Token amount not calculated yet. Please wait.');
|
setError('Payment amount not ready. Please wait.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wallet || !wallet.adapter) {
|
||||||
|
setError('Wallet not connected.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,23 +103,42 @@ export default function PaymentModal({
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenAmountBN = new BN(tokenAmount);
|
const paymentRequest: PaymentRequest = {
|
||||||
|
paymentMethod: paymentMethod,
|
||||||
|
amount: tokenAmount,
|
||||||
|
};
|
||||||
|
|
||||||
const result = await sendSolanaTokenPayment(connection, walletState.publicKey!, tokenAmountBN, walletState.walletType!);
|
// Use different RPC connection based on payment method
|
||||||
|
const connectionToUse = paymentMethod === PaymentMethod.NAT_GOR ? new Connection(GORBAGANA_RPC_URL!) : solanaConnection;
|
||||||
|
|
||||||
|
const result = await sendSolanaPayment(
|
||||||
|
wallet.adapter,
|
||||||
|
connectionToUse,
|
||||||
|
publicKey!.toBase58(),
|
||||||
|
paymentRequest
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success && result.transactionSignature) {
|
if (result.success && result.transactionSignature) {
|
||||||
onPaymentComplete(result.transactionSignature);
|
onPaymentComplete(result.transactionSignature, paymentMethod);
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || `${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment failed. Please try again.`);
|
setError(result.error || 'Payment failed. Please try again.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
|
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [connection, walletState, tokenAmount, loadingPrice, onPaymentComplete]);
|
}, [paymentMethod, tokenAmount, loadingPrice, wallet, solanaConnection, publicKey, onPaymentComplete]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
const getPaymentAmountDisplay = () => {
|
||||||
|
if (loadingPrice) {
|
||||||
|
return 'Loading...'
|
||||||
|
};
|
||||||
|
|
||||||
|
return tokenAmount > 0 ? `${(tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6)}` : '0';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen || !paymentMethod) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}>
|
<div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}>
|
||||||
@ -90,7 +146,7 @@ export default function PaymentModal({
|
|||||||
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||||
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
|
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
|
||||||
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
|
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||||
Complete {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment
|
Complete Payment
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -102,10 +158,23 @@ export default function PaymentModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Method Display */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--foreground)' }}>
|
||||||
|
Payment Method
|
||||||
|
</label>
|
||||||
|
<div className="p-3 rounded-md" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{PAYMENT_METHOD_LABELS[paymentMethod]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Details */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
|
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
|
||||||
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
|
||||||
<code className="text-sm font-mono break-all block">{recipientAddress}</code>
|
<code className="text-sm font-mono break-all block">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -117,7 +186,7 @@ export default function PaymentModal({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={`$${targetUsdAmount}`}
|
value={targetUsdAmount}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="w-full p-3 pr-12 rounded-md"
|
className="w-full p-3 pr-12 rounded-md"
|
||||||
style={{
|
style={{
|
||||||
@ -149,9 +218,9 @@ export default function PaymentModal({
|
|||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tokenAmount > 0 ? (tokenAmount / Math.pow(10, tokenDecimals)).toFixed(6) : '0'}
|
value={getPaymentAmountDisplay()}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="w-full p-3 pr-12 rounded-md"
|
className="w-full p-3 pr-20 rounded-md"
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--card-bg)',
|
background: 'var(--card-bg)',
|
||||||
border: '1px solid var(--input-border)',
|
border: '1px solid var(--input-border)',
|
||||||
@ -161,16 +230,20 @@ export default function PaymentModal({
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!loadingPrice && (
|
{!(paymentMethod === PaymentMethod.SPL_TOKEN && loadingPrice) && (
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
<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)' }}>{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL}</span>
|
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>
|
||||||
|
{paymentMethod === PaymentMethod.NAT_GOR ? 'GOR (native)' : tokenSymbol}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{paymentMethod === PaymentMethod.SPL_TOKEN && (
|
||||||
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
|
||||||
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS}`}>Jupiter</a>
|
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@ -209,7 +282,9 @@ 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>
|
<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>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{loadingPrice ? 'Loading Price...' : loading ? 'Processing...' : 'Pay with Solana Wallet'}
|
{loadingPrice ? 'Loading Price...' :
|
||||||
|
loading ? 'Processing...' :
|
||||||
|
'Pay with Solana Wallet'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { connectSolanaWallet, disconnectSolanaWallet, checkSolanaWalletConnection } from '@/services/solana';
|
|
||||||
import { SolanaWalletType, SolanaWalletState } from '@/types';
|
|
||||||
|
|
||||||
interface SolanaConnectProps {
|
|
||||||
onConnect: (walletState: SolanaWalletState) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SolanaConnect({ onConnect }: SolanaConnectProps) {
|
|
||||||
const [connecting, setConnecting] = useState(false);
|
|
||||||
const [walletState, setWalletState] = useState<SolanaWalletState>({
|
|
||||||
connected: false,
|
|
||||||
publicKey: null,
|
|
||||||
walletType: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleConnect = async (walletType: SolanaWalletType) => {
|
|
||||||
setConnecting(true);
|
|
||||||
try {
|
|
||||||
const newWalletState = await connectSolanaWallet(walletType);
|
|
||||||
setWalletState(newWalletState);
|
|
||||||
onConnect(newWalletState);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to Solana wallet:', error);
|
|
||||||
alert(error instanceof Error ? error.message : 'Failed to connect wallet');
|
|
||||||
} finally {
|
|
||||||
setConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = async () => {
|
|
||||||
if (walletState.walletType) {
|
|
||||||
try {
|
|
||||||
await disconnectSolanaWallet(walletState.walletType);
|
|
||||||
const disconnectedState = {
|
|
||||||
connected: false,
|
|
||||||
publicKey: null,
|
|
||||||
walletType: null
|
|
||||||
};
|
|
||||||
setWalletState(disconnectedState);
|
|
||||||
onConnect(disconnectedState);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to disconnect wallet:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check for auto-connection on page load
|
|
||||||
const checkConnection = () => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// Check Phantom
|
|
||||||
if (window.phantom?.solana && checkSolanaWalletConnection('phantom')) {
|
|
||||||
handleConnect('phantom');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Solflare
|
|
||||||
if (window.solflare && checkSolanaWalletConnection('solflare')) {
|
|
||||||
handleConnect('solflare');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkConnection();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
|
||||||
{walletState.connected ? (
|
|
||||||
<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 ({walletState.walletType})
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full p-3 rounded-md mb-3" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
|
||||||
<p className="text-sm font-mono break-all text-center">{walletState.publicKey}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleDisconnect}
|
|
||||||
className="px-4 py-2 rounded-md text-sm transition-colors"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'var(--muted)',
|
|
||||||
color: 'var(--foreground)',
|
|
||||||
border: '1px solid var(--input-border)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-full space-y-3">
|
|
||||||
<button
|
|
||||||
onClick={() => handleConnect('phantom')}
|
|
||||||
disabled={connecting || !window.phantom?.solana}
|
|
||||||
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
|
|
||||||
style={{
|
|
||||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
|
||||||
color: 'var(--primary-foreground)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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 Phantom Wallet'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => handleConnect('solflare')}
|
|
||||||
disabled={connecting || !window.solflare}
|
|
||||||
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
|
|
||||||
style={{
|
|
||||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
|
||||||
color: 'var(--primary-foreground)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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 Solflare Wallet'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{!window.phantom?.solana && !window.solflare && (
|
|
||||||
<p className="text-sm text-center" style={{ color: 'var(--muted)' }}>
|
|
||||||
Please install Phantom or Solflare wallet extension
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -9,8 +9,6 @@ interface StatusDisplayProps {
|
|||||||
dns?: string;
|
dns?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
repoUrl?: string;
|
repoUrl?: string;
|
||||||
commitHash?: string;
|
|
||||||
shortCommitHash?: string;
|
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,8 +21,6 @@ export default function StatusDisplay({
|
|||||||
dns,
|
dns,
|
||||||
appName,
|
appName,
|
||||||
repoUrl,
|
repoUrl,
|
||||||
commitHash,
|
|
||||||
shortCommitHash,
|
|
||||||
error,
|
error,
|
||||||
}: StatusDisplayProps) {
|
}: StatusDisplayProps) {
|
||||||
// Get domain suffix from environment variable
|
// Get domain suffix from environment variable
|
||||||
|
70
src/components/WalletProviders.tsx
Normal file
70
src/components/WalletProviders.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
|
||||||
|
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
|
||||||
|
import { BackpackWalletAdapter } from '@solana/wallet-adapter-backpack';
|
||||||
|
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom';
|
||||||
|
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare';
|
||||||
|
|
||||||
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
|
// Default styles that can be overridden by your app
|
||||||
|
import '@solana/wallet-adapter-react-ui/styles.css';
|
||||||
|
|
||||||
|
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||||
|
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
|
||||||
|
|
||||||
|
interface WalletProvidersProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WalletProviders({ children }: WalletProvidersProps) {
|
||||||
|
const { selectedPaymentMethod } = usePaymentMethod();
|
||||||
|
|
||||||
|
// Configure the Solana network endpoint
|
||||||
|
const endpoint = useMemo(() => {
|
||||||
|
return SOLANA_RPC_URL;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// All available wallet adapters
|
||||||
|
const allWallets = useMemo(
|
||||||
|
() => [
|
||||||
|
new PhantomWalletAdapter(),
|
||||||
|
new SolflareWalletAdapter(),
|
||||||
|
new BackpackWalletAdapter()
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter wallets based on selected payment method
|
||||||
|
const wallets = useMemo(() => {
|
||||||
|
if (!selectedPaymentMethod) {
|
||||||
|
// If no payment method selected, do not show any wallets
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return allWallets.filter(wallet => {
|
||||||
|
const isBackpack = wallet.name.toLowerCase().includes('backpack');
|
||||||
|
|
||||||
|
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
|
return isBackpack; // Only Backpack for native GOR
|
||||||
|
} else {
|
||||||
|
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [allWallets, selectedPaymentMethod]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConnectionProvider endpoint={endpoint}>
|
||||||
|
<WalletProvider wallets={wallets} autoConnect>
|
||||||
|
<WalletModalProvider>
|
||||||
|
{children}
|
||||||
|
</WalletModalProvider>
|
||||||
|
</WalletProvider>
|
||||||
|
</ConnectionProvider>
|
||||||
|
);
|
||||||
|
}
|
11
src/constants/payments.ts
Normal file
11
src/constants/payments.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { PaymentMethod } from "@/types";
|
||||||
|
|
||||||
|
// Payment method labels for UI
|
||||||
|
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
|
||||||
|
[PaymentMethod.NAT_GOR]: 'GOR (native)',
|
||||||
|
[PaymentMethod.SPL_TOKEN]: process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL!
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SOLANA_GOR_MINT_ADDRESS = '71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg';
|
||||||
|
|
||||||
|
export const IS_NAT_GOR_TRANSFER_ENABLED = process.env.NEXT_PUBLIC_ENABLE_NATIVE_GOR_TRANSFER === "true";
|
39
src/contexts/PaymentMethodContext.tsx
Normal file
39
src/contexts/PaymentMethodContext.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
|
||||||
|
interface PaymentMethodContextType {
|
||||||
|
selectedPaymentMethod: PaymentMethod | null;
|
||||||
|
setSelectedPaymentMethod: (method: PaymentMethod | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentMethodContext = createContext<PaymentMethodContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface PaymentMethodProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PaymentMethodProvider({ children }: PaymentMethodProviderProps) {
|
||||||
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentMethodContext.Provider
|
||||||
|
value={{
|
||||||
|
selectedPaymentMethod,
|
||||||
|
setSelectedPaymentMethod
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PaymentMethodContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePaymentMethod() {
|
||||||
|
const context = useContext(PaymentMethodContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('usePaymentMethod must be used within a PaymentMethodProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
@ -12,7 +12,7 @@ interface TokenPriceInfo {
|
|||||||
decimals: number;
|
decimals: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequiredTokenInfo {
|
export interface RequiredTokenInfo {
|
||||||
requiredAmountInBaseUnits: number;
|
requiredAmountInBaseUnits: number;
|
||||||
decimals: number;
|
decimals: number;
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ interface RequiredTokenInfo {
|
|||||||
* @param mintAddress The Solana token mint address
|
* @param mintAddress The Solana token mint address
|
||||||
* @returns Token price information including USD price and decimals
|
* @returns Token price information including USD price and decimals
|
||||||
*/
|
*/
|
||||||
export async function getTokenInfo(mintAddress: string): Promise<TokenPriceInfo> {
|
async function getTokenInfo(mintAddress: string): Promise<TokenPriceInfo> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://lite-api.jup.ag/price/v3?ids=${mintAddress}`);
|
const response = await fetch(`https://lite-api.jup.ag/price/v3?ids=${mintAddress}`);
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ export async function getRequiredTokenInfo(targetUsdAmount: number, mintAddress:
|
|||||||
const tokenAmount = targetUsdAmount / priceInfo.usdPrice;
|
const tokenAmount = targetUsdAmount / priceInfo.usdPrice;
|
||||||
|
|
||||||
// Convert to smallest units (considering decimals)
|
// Convert to smallest units (considering decimals)
|
||||||
const amountInSmallestUnits = Math.round(tokenAmount * Math.pow(10, priceInfo.decimals));
|
const requiredAmountInBaseUnits = Math.round(tokenAmount * Math.pow(10, priceInfo.decimals));
|
||||||
|
|
||||||
return {requiredAmountInBaseUnits: amountInSmallestUnits, decimals: priceInfo.decimals};
|
return {requiredAmountInBaseUnits, decimals: priceInfo.decimals};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { CreateRecordResponse } from '../types';
|
import { CreateRecordResponse } from '../types';
|
||||||
|
import { PaymentMethod } from '../types';
|
||||||
|
|
||||||
export const createApplicationDeploymentRequest = async (
|
export const createApplicationDeploymentRequest = async (
|
||||||
url: string,
|
url: string,
|
||||||
txHash: string,
|
txHash: string,
|
||||||
|
paymentMethod: PaymentMethod,
|
||||||
): Promise<CreateRecordResponse> => {
|
): Promise<CreateRecordResponse> => {
|
||||||
try {
|
try {
|
||||||
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment`);
|
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment`);
|
||||||
@ -13,7 +15,7 @@ export const createApplicationDeploymentRequest = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ url, txHash }),
|
body: JSON.stringify({ url, txHash, paymentMethod }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
|
import { Connection, PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
||||||
import {
|
import {
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
createTransferInstruction,
|
createTransferInstruction,
|
||||||
createAssociatedTokenAccountInstruction,
|
createAssociatedTokenAccountInstruction,
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
getAssociatedTokenAddressSync
|
||||||
} from '@solana/spl-token';
|
} from '@solana/spl-token';
|
||||||
|
import { WalletAdapter } from '@solana/wallet-adapter-base';
|
||||||
|
|
||||||
import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types';
|
import { SolanaPaymentResult, PaymentRequest, PaymentMethod } from '../types';
|
||||||
|
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS, 'SOLANA_TOKEN_MINT_ADDRESS is required');
|
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS, 'SOLANA_TOKEN_MINT_ADDRESS is required');
|
||||||
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
|
assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
|
||||||
@ -16,59 +18,7 @@ assert(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_REC
|
|||||||
const TOKEN_MINT = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
|
const TOKEN_MINT = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
|
||||||
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
||||||
|
|
||||||
export const connectSolanaWallet = async (walletType: SolanaWalletType): Promise<SolanaWalletState> => {
|
// Helper function to find associated token address
|
||||||
try {
|
|
||||||
if (walletType === 'phantom') {
|
|
||||||
if (!window.phantom?.solana) {
|
|
||||||
throw new Error('Phantom wallet not found. Please install Phantom browser extension.');
|
|
||||||
}
|
|
||||||
const response = await window.phantom.solana.connect();
|
|
||||||
return {
|
|
||||||
connected: true,
|
|
||||||
publicKey: response.publicKey.toString(),
|
|
||||||
walletType
|
|
||||||
};
|
|
||||||
} else if (walletType === 'solflare') {
|
|
||||||
if (!window.solflare) {
|
|
||||||
throw new Error('Solflare wallet not found. Please install Solflare browser extension.');
|
|
||||||
}
|
|
||||||
await window.solflare.connect();
|
|
||||||
const publicKey = window.solflare.publicKey?.toString();
|
|
||||||
if (!publicKey) {
|
|
||||||
throw new Error('Failed to get public key from Solflare wallet');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
connected: true,
|
|
||||||
publicKey,
|
|
||||||
walletType
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unsupported wallet type: ${walletType}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to Solana wallet:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const disconnectSolanaWallet = async (walletType: SolanaWalletType): Promise<void> => {
|
|
||||||
try {
|
|
||||||
let wallet = null;
|
|
||||||
|
|
||||||
if (walletType === 'phantom') {
|
|
||||||
wallet = window.phantom?.solana;
|
|
||||||
} else if (walletType === 'solflare') {
|
|
||||||
wallet = window.solflare;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallet && wallet.disconnect) {
|
|
||||||
await wallet.disconnect();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to disconnect Solana wallet:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function findAssociatedTokenAddress(
|
async function findAssociatedTokenAddress(
|
||||||
walletAddress: PublicKey,
|
walletAddress: PublicKey,
|
||||||
tokenMintAddress: PublicKey
|
tokenMintAddress: PublicKey
|
||||||
@ -83,37 +33,90 @@ async function findAssociatedTokenAddress(
|
|||||||
)[0];
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WalletAdapter {
|
// Send native GOR payment
|
||||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
export async function sendNativeGorPayment(
|
||||||
}
|
wallet: WalletAdapter,
|
||||||
|
|
||||||
export async function sendSolanaTokenPayment(
|
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
walletPublicKey: string,
|
walletPublicKey: string,
|
||||||
tokenAmount: BN,
|
tokenAmount: BN
|
||||||
walletType: SolanaWalletType
|
|
||||||
): Promise<SolanaPaymentResult> {
|
): Promise<SolanaPaymentResult> {
|
||||||
try {
|
try {
|
||||||
let wallet: WalletAdapter | null = null;
|
if (!wallet.connected || !wallet.publicKey) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
if (walletType === 'phantom') {
|
|
||||||
wallet = window.phantom?.solana || null;
|
|
||||||
} else if (walletType === 'solflare') {
|
|
||||||
wallet = window.solflare || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wallet) {
|
const senderPublicKey = new PublicKey(walletPublicKey);
|
||||||
throw new Error(`${walletType} wallet not found`);
|
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||||
|
|
||||||
|
console.log('Processing native GOR payment:', {
|
||||||
|
sender: senderPublicKey.toBase58(),
|
||||||
|
receiver: receiverPublicKey.toBase58(),
|
||||||
|
amount: tokenAmount.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const transaction = new Transaction().add(
|
||||||
|
SystemProgram.transfer({
|
||||||
|
fromPubkey: senderPublicKey,
|
||||||
|
toPubkey: receiverPublicKey,
|
||||||
|
lamports: BigInt(tokenAmount.toString()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set transaction details
|
||||||
|
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
|
||||||
|
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||||
|
transaction.feePayer = senderPublicKey;
|
||||||
|
|
||||||
|
console.log('Sending native GOR transaction...');
|
||||||
|
const signature = await wallet.sendTransaction(transaction, connection);
|
||||||
|
console.log('Native GOR transaction sent:', signature);
|
||||||
|
|
||||||
|
// Confirm transaction
|
||||||
|
const confirmation = await connection.confirmTransaction({
|
||||||
|
signature,
|
||||||
|
blockhash: latestBlockhash.blockhash,
|
||||||
|
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||||
|
}, 'confirmed');
|
||||||
|
|
||||||
|
if (confirmation.value.err) {
|
||||||
|
console.error('Transaction error:', confirmation.value.err);
|
||||||
|
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionSignature: signature
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Native GOR payment error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Native GOR payment failed'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send SPL token payment
|
||||||
|
export async function sendSplTokenPayment(
|
||||||
|
wallet: WalletAdapter,
|
||||||
|
connection: Connection,
|
||||||
|
walletPublicKey: string,
|
||||||
|
tokenAmount: BN
|
||||||
|
): Promise<SolanaPaymentResult> {
|
||||||
|
try {
|
||||||
|
if (!wallet.connected || !wallet.publicKey) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
}
|
}
|
||||||
|
|
||||||
const senderPublicKey = new PublicKey(walletPublicKey);
|
const senderPublicKey = new PublicKey(walletPublicKey);
|
||||||
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
||||||
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||||
|
|
||||||
console.log('Processing payment with keys:', {
|
console.log('Processing SPL token payment:', {
|
||||||
sender: senderPublicKey.toBase58(),
|
sender: senderPublicKey.toBase58(),
|
||||||
mint: mintPublicKey.toBase58(),
|
mint: mintPublicKey.toBase58(),
|
||||||
receiver: receiverPublicKey.toBase58(),
|
receiver: receiverPublicKey.toBase58(),
|
||||||
|
amount: tokenAmount.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
const senderATA = await findAssociatedTokenAddress(
|
const senderATA = await findAssociatedTokenAddress(
|
||||||
@ -182,9 +185,9 @@ export async function sendSolanaTokenPayment(
|
|||||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||||
transaction.feePayer = senderPublicKey;
|
transaction.feePayer = senderPublicKey;
|
||||||
|
|
||||||
console.log('Sending transaction...');
|
console.log('Sending SPL token transaction...');
|
||||||
const { signature } = await wallet.signAndSendTransaction(transaction);
|
const signature = await wallet.sendTransaction(transaction, connection);
|
||||||
console.log('Transaction sent:', signature);
|
console.log('SPL token transaction sent:', signature);
|
||||||
|
|
||||||
// Confirm transaction
|
// Confirm transaction
|
||||||
const confirmation = await connection.confirmTransaction({
|
const confirmation = await connection.confirmTransaction({
|
||||||
@ -202,6 +205,37 @@ export async function sendSolanaTokenPayment(
|
|||||||
success: true,
|
success: true,
|
||||||
transactionSignature: signature
|
transactionSignature: signature
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SPL token payment error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'SPL token payment failed'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main payment function that routes to the appropriate payment method
|
||||||
|
export async function sendSolanaPayment(
|
||||||
|
wallet: WalletAdapter,
|
||||||
|
connection: Connection,
|
||||||
|
walletPublicKey: string,
|
||||||
|
paymentRequest: PaymentRequest
|
||||||
|
): Promise<SolanaPaymentResult> {
|
||||||
|
try {
|
||||||
|
if (!wallet.connected || !wallet.publicKey) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenAmount = new BN(paymentRequest.amount);
|
||||||
|
|
||||||
|
switch (paymentRequest.paymentMethod) {
|
||||||
|
case PaymentMethod.NAT_GOR:
|
||||||
|
return await sendNativeGorPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||||
|
case PaymentMethod.SPL_TOKEN:
|
||||||
|
return await sendSplTokenPayment(wallet, connection, walletPublicKey, tokenAmount);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported payment method: ${paymentRequest.paymentMethod}`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Payment error:', error);
|
console.error('Payment error:', error);
|
||||||
return {
|
return {
|
||||||
@ -209,18 +243,25 @@ export async function sendSolanaTokenPayment(
|
|||||||
error: error instanceof Error ? error.message : 'Payment failed'
|
error: error instanceof Error ? error.message : 'Payment failed'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Helper function to check wallet connection status
|
// Helper function to get recipient address based on payment method
|
||||||
export const checkSolanaWalletConnection = (walletType: SolanaWalletType): boolean => {
|
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
|
||||||
try {
|
switch (paymentMethod) {
|
||||||
if (walletType === 'phantom') {
|
case PaymentMethod.NAT_GOR:
|
||||||
return window.phantom?.solana?.isConnected || false;
|
// For native GOR payments, use the direct recipient address
|
||||||
} else if (walletType === 'solflare') {
|
return PAYMENT_RECEIVER_ADDRESS;
|
||||||
return window.solflare?.isConnected || false;
|
case PaymentMethod.SPL_TOKEN:
|
||||||
|
// For SPL token payments, use the associated token account
|
||||||
|
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||||
|
const mintPublicKey = new PublicKey(TOKEN_MINT);
|
||||||
|
const associatedTokenAddress = getAssociatedTokenAddressSync(
|
||||||
|
mintPublicKey,
|
||||||
|
receiverPublicKey,
|
||||||
|
true // Allow off-curve addresses
|
||||||
|
);
|
||||||
|
return associatedTokenAddress.toBase58();
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported payment method: ${paymentMethod}`);
|
||||||
}
|
}
|
||||||
return false;
|
}
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -1,25 +1,8 @@
|
|||||||
import { Transaction } from "@solana/web3.js";
|
// Payment method types
|
||||||
|
export enum PaymentMethod {
|
||||||
// extend the global Window interface to include Solana wallets
|
NAT_GOR = 'nat-gor',
|
||||||
declare global {
|
SPL_TOKEN = 'spl-token'
|
||||||
interface Window {
|
|
||||||
phantom?: {
|
|
||||||
solana?: {
|
|
||||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
|
||||||
connect(): Promise<{ publicKey: { toString(): string } }>;
|
|
||||||
disconnect(): Promise<void>;
|
|
||||||
isConnected: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
solflare?: {
|
|
||||||
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>;
|
|
||||||
connect(): Promise<{ publicKey: { toString(): string } }>;
|
|
||||||
disconnect(): Promise<void>;
|
|
||||||
isConnected: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
chainId: string;
|
chainId: string;
|
||||||
rpcEndpoint: string;
|
rpcEndpoint: string;
|
||||||
@ -45,7 +28,6 @@ export interface CreateRecordResponse {
|
|||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SolanaWalletType = 'phantom' | 'solflare';
|
|
||||||
|
|
||||||
export interface SolanaPaymentResult {
|
export interface SolanaPaymentResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -57,14 +39,13 @@ export interface PaymentModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
url: string;
|
url: string;
|
||||||
onPaymentComplete: (txHash: string) => void;
|
onPaymentComplete: (txHash: string, paymentMethod: PaymentMethod) => void;
|
||||||
walletState: SolanaWalletState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SolanaWalletState {
|
|
||||||
connected: boolean;
|
export interface PaymentRequest {
|
||||||
publicKey: string | null;
|
paymentMethod: PaymentMethod;
|
||||||
walletType: SolanaWalletType | null;
|
amount: number; // in base units (lamports for native GOR, token base units for SPL)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LaconicTransferResult {
|
export interface LaconicTransferResult {
|
||||||
|
@ -1,32 +1,76 @@
|
|||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
|
|
||||||
import { Connection, PublicKey } from '@solana/web3.js';
|
import { Connection, ParsedInstruction, PartiallyDecodedInstruction } from '@solana/web3.js';
|
||||||
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
|
||||||
// Simplified transaction info extraction following reference implementation
|
import { getRecipientAddress } from '@/services/solana';
|
||||||
const extractTxInfo = async (connection: Connection, transactionSignature: string): Promise<{ authority: string; amount: string }> => {
|
import { PaymentMethod } from '../types';
|
||||||
|
|
||||||
|
// Extract transaction info for native GOR transfers
|
||||||
|
const extractTxInfo = async (
|
||||||
|
connection: Connection,
|
||||||
|
transactionSignature: string,
|
||||||
|
paymentMethod: PaymentMethod
|
||||||
|
): Promise<{ authority: string; amount: string; destination: string }> => {
|
||||||
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Transaction not found');
|
throw new Error('Transaction not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const transferInstruction = result.transaction.message.instructions.find(
|
let transferInstruction: ParsedInstruction | PartiallyDecodedInstruction | undefined;
|
||||||
|
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentMethod.NAT_GOR:
|
||||||
|
// Look for system program transfer instruction
|
||||||
|
transferInstruction = result.transaction.message.instructions.find(
|
||||||
|
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||||
|
throw new Error('Native GOR transfer instruction not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { info: { lamports, source, destination } } = transferInstruction.parsed;
|
||||||
|
return { authority: source, amount: lamports.toString(), destination };
|
||||||
|
|
||||||
|
case PaymentMethod.SPL_TOKEN:
|
||||||
|
// Look for token transfer instruction using TOKEN_PROGRAM_ID
|
||||||
|
transferInstruction = result.transaction.message.instructions.find(
|
||||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||||
throw new Error('Transfer instruction not found');
|
throw new Error('SPL token transfer instruction not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { info: { amount, authority } } = transferInstruction.parsed;
|
const parsed = transferInstruction.parsed;
|
||||||
return { authority, amount };
|
|
||||||
|
// Handle both transferChecked and transfer types
|
||||||
|
if (parsed.type === 'transferChecked') {
|
||||||
|
const { info: { tokenAmount, authority, destination } } = parsed;
|
||||||
|
return {
|
||||||
|
authority,
|
||||||
|
amount: tokenAmount.amount,
|
||||||
|
destination
|
||||||
|
};
|
||||||
|
} else if (parsed.type === 'transfer') {
|
||||||
|
const { info: { amount, authority, destination } } = parsed;
|
||||||
|
return { authority, amount, destination };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unsupported token transfer type');
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid payment method');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyUnusedSolanaPayment = async (
|
export const verifyUnusedSolanaPayment = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
transactionSignature: string,
|
transactionSignature: string,
|
||||||
tokenAmount: BN
|
expectedAmount: BN,
|
||||||
|
paymentMethod: PaymentMethod,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
valid: boolean,
|
valid: boolean,
|
||||||
reason?: string,
|
reason?: string,
|
||||||
@ -74,45 +118,28 @@ export const verifyUnusedSolanaPayment = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract transaction info using simplified approach
|
// Extract transaction info based on payment method
|
||||||
const { amount, authority } = await extractTxInfo(connection, transactionSignature);
|
const transferInfo = await extractTxInfo(connection, transactionSignature, paymentMethod);
|
||||||
|
const amount = transferInfo.amount;
|
||||||
|
const authority = transferInfo.authority;
|
||||||
|
const destination = transferInfo.destination;
|
||||||
|
|
||||||
// Verify amount using BN comparison like in reference
|
// Verify amount using BN comparison
|
||||||
const transactionAmount = new BN(amount);
|
const transactionAmount = new BN(amount);
|
||||||
if (transactionAmount.lt(tokenAmount)) {
|
if (transactionAmount.lt(expectedAmount)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
reason: `Payment amount (${amount}) is less than required (${tokenAmount.toString()})`
|
reason: `Payment amount (${amount}) is less than required (${expectedAmount.toString()})`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedTokenAccount = getAssociatedTokenAddressSync(
|
// Verify recipient address
|
||||||
new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!),
|
const expectedRecipientAddress = getRecipientAddress(paymentMethod);
|
||||||
new PublicKey(process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS!),
|
|
||||||
// Allow recipient account address to be off the curve (i.e now having a private key)
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify recipient address by checking the transaction instructions
|
if (destination !== expectedRecipientAddress) {
|
||||||
let foundValidTransfer = false;
|
|
||||||
|
|
||||||
for (const instruction of transactionResult.transaction.message.instructions) {
|
|
||||||
if ('parsed' in instruction && instruction.programId.equals(TOKEN_PROGRAM_ID)) {
|
|
||||||
const parsed = instruction.parsed;
|
|
||||||
if (parsed.type === 'transferChecked' || parsed.type === 'transfer') {
|
|
||||||
// Verify amount and recipient's associated token address
|
|
||||||
if (parsed.info.amount === amount && parsed.info.destination === expectedTokenAccount.toBase58() ) {
|
|
||||||
foundValidTransfer = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundValidTransfer) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
reason: 'Valid Solana token transfer not found in transaction'
|
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user