Compare commits
10 Commits
sk-test-rp
...
main
Author | SHA1 | Date | |
---|---|---|---|
41add0a99a | |||
ed08ace0a4 | |||
ac5dfe6966 | |||
bc0d10d4c3 | |||
f6c6147340 | |||
452c4db5f8 | |||
fbe4eed31d | |||
b4c6b0aa4e | |||
ac9d4ea8eb | |||
acabd4569f |
32
.env.example
Normal file
32
.env.example
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Client-side environment variables must be prefixed with NEXT_PUBLIC_
|
||||||
|
|
||||||
|
# Solana Payment Configuration
|
||||||
|
# TODO: Use different RPC URL
|
||||||
|
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_SYMBOL=GOR
|
||||||
|
|
||||||
|
# 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
|
||||||
|
NEXT_PUBLIC_EXAMPLE_URL=https://git.vdb.to/cerc-io/test-progressive-web-app
|
||||||
|
|
||||||
|
# Laconic Registry Configuration
|
||||||
|
NEXT_PUBLIC_REGISTRY_CHAIN_ID=laconic-mainnet
|
||||||
|
NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT=https://laconicd-mainnet-1.laconic.com
|
||||||
|
NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT=https://laconicd-mainnet-1.laconic.com/graphql
|
||||||
|
NEXT_PUBLIC_ALNT_COST_LRN=lrn://laconic/pricing/alnt
|
||||||
|
NEXT_PUBLIC_DEPLOYMENT_COST_LRN=lrn://laconic/pricing/webapp-deployment
|
||||||
|
REGISTRY_GAS_PRICE=0.001
|
||||||
|
REGISTRY_AUTHORITY=laconic-deploy
|
||||||
|
REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
|
||||||
|
REGISTRY_USER_KEY=
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
DEPLOYER_LRN=lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
||||||
|
NEXT_PUBLIC_DOMAIN_SUFFIX=apps.vaasl.io
|
@ -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"
|
|
||||||
}
|
|
||||||
};
|
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -31,11 +31,18 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env.local
|
||||||
|
.env
|
||||||
|
|
||||||
|
# registry env files
|
||||||
|
.registry.env
|
||||||
|
.app.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
|
||||||
|
# Reveal file out dir
|
||||||
|
out
|
305
CLAUDE.md
Normal file
305
CLAUDE.md
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
# CLAUDE.md - GOR Deploy Development Guide
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start development server (http://localhost:3000)
|
||||||
|
npm run build # Build for production
|
||||||
|
npm start # Start production server
|
||||||
|
npm run lint # Run ESLint
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
# No test framework configured - check README for testing approach
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Architecture
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **Frontend**: Next.js 15.3.1 with React 19
|
||||||
|
- **Styling**: TailwindCSS 4
|
||||||
|
- **Blockchain**:
|
||||||
|
- Solana Web3.js for payments
|
||||||
|
- @cerc-io/registry-sdk for Laconic Registry
|
||||||
|
- CosmJS for Cosmos blockchain interactions
|
||||||
|
- **Wallet Integration**: Phantom, Solflare & Backpack with context-based filtering
|
||||||
|
- **Language**: TypeScript
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
- `@solana/web3.js` - Solana blockchain interactions
|
||||||
|
- `@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
|
||||||
|
- `@cosmjs/stargate` - Cosmos blockchain operations
|
||||||
|
- `axios` - HTTP requests for GitHub API
|
||||||
|
- `bn.js` - Big number arithmetic
|
||||||
|
- `big.js` - Decimal arithmetic
|
||||||
|
|
||||||
|
## Application Flow
|
||||||
|
|
||||||
|
1. **Payment Method Selection**: User chooses between native GOR or SPL token payment
|
||||||
|
2. **Wallet Connection**: Compatible wallets are filtered based on payment method
|
||||||
|
3. **URL Input**: User enters repository URL to deploy (enabled after wallet connection)
|
||||||
|
4. **Payment**: Fixed amount payment in native GOR or GOR SPL tokens to configured recipient
|
||||||
|
5. **Verification**: Server verifies Solana transaction (5-minute window)
|
||||||
|
6. **LNT Transfer**: Server transfers LNT tokens for registry fees
|
||||||
|
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
|
||||||
|
|
||||||
|
### Frontend Components
|
||||||
|
- `src/app/page.tsx` - Main application page with 4-step flow (Payment Method → Wallet → URL → Status)
|
||||||
|
- `src/components/PaymentModal.tsx` - Solana payment interface with pre-selected payment method
|
||||||
|
- `src/components/WalletProviders.tsx` - Context-aware wallet provider with filtering
|
||||||
|
- `src/components/URLForm.tsx` - URL input form
|
||||||
|
- `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
|
||||||
|
- `src/services/solana.ts` - Unified wallet adapter integration for payments
|
||||||
|
- `src/services/registry.ts` - Client-side registry service wrapper
|
||||||
|
- `src/services/laconic-transfer.ts` - LNT token transfer logic
|
||||||
|
- `src/utils/solana-verify.ts` - Payment verification with replay protection
|
||||||
|
- `src/config/index.ts` - Registry configuration management
|
||||||
|
- `src/constants/payments.ts` - Payment method types and configuration
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
- `src/app/api/registry/route.ts` - Main server-side registry operations with dual payment support
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### Client-side Variables (NEXT_PUBLIC_*)
|
||||||
|
```bash
|
||||||
|
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_RECIPIENT_ADDRESS=<RECIPIENT_WALLET>
|
||||||
|
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR
|
||||||
|
NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD=5
|
||||||
|
NEXT_PUBLIC_DOMAIN_SUFFIX=.example.com
|
||||||
|
NEXT_PUBLIC_EXAMPLE_URL=https://git.vdb.to/cerc-io/test-progressive-web-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-side Variables
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_REGISTRY_CHAIN_ID=laconic-mainnet
|
||||||
|
NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT=https://laconicd-mainnet-1.laconic.com/api
|
||||||
|
NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT=https://laconicd-mainnet-1.laconic.com
|
||||||
|
REGISTRY_BOND_ID=<BOND_ID>
|
||||||
|
REGISTRY_AUTHORITY=<AUTHORITY_NAME>
|
||||||
|
REGISTRY_USER_KEY=<PRIVATE_KEY>
|
||||||
|
APP_NAME=gor-deploy
|
||||||
|
DEPLOYER_LRN=<DEPLOYER_LRN>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Patterns and Conventions
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- Uses React Context (`PaymentMethodContext`) for global payment method state
|
||||||
|
- React `useState` for component-level state
|
||||||
|
- 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
|
||||||
|
- Comprehensive try-catch blocks in all async operations
|
||||||
|
- User-friendly error messages in UI
|
||||||
|
- Server-side error logging with detailed context
|
||||||
|
- Wallet compatibility validation with clear feedback
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
- Environment variables for sensitive data
|
||||||
|
- Transaction verification with replay protection
|
||||||
|
- Proper SPL token account creation and validation
|
||||||
|
- Private key never exposed to client-side
|
||||||
|
- Wallet signature verification via adapters
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Adding New Features
|
||||||
|
1. **Client Components**: Add to `src/components/`
|
||||||
|
2. **Services**: Add to `src/services/`
|
||||||
|
3. **API Routes**: Add to `src/app/api/`
|
||||||
|
4. **Types**: Update `src/types/index.ts`
|
||||||
|
5. **Payment Methods**: Update `src/constants/payments.ts`
|
||||||
|
|
||||||
|
### Payment Integration
|
||||||
|
- All payment logic unified in `src/services/solana.ts`
|
||||||
|
- Verification logic in `src/utils/solana-verify.ts`
|
||||||
|
- 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 operations in `src/app/api/registry/route.ts`
|
||||||
|
- Configuration in `src/config/index.ts`
|
||||||
|
- LRN format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
||||||
|
|
||||||
|
## DNS and Resource Naming
|
||||||
|
|
||||||
|
### DNS Name Generation
|
||||||
|
Format: `{sanitized-repo-name}-{7-char-commit-hash}`
|
||||||
|
- Sanitized to be DNS-compatible (alphanumeric + dashes)
|
||||||
|
- Max 63 characters per DNS label
|
||||||
|
- Always lowercase
|
||||||
|
- Random salt removed (commit hash provides uniqueness)
|
||||||
|
|
||||||
|
### LRN (Laconic Resource Name) Format
|
||||||
|
Format: `lrn://{authority}/applications/{app-name}-{commit-hash}`
|
||||||
|
- Matches DNS naming for consistency
|
||||||
|
- Includes both versioned and unversioned mappings
|
||||||
|
|
||||||
|
## Transaction Flow Details
|
||||||
|
|
||||||
|
### Solana Payment Verification
|
||||||
|
- 5-minute transaction window
|
||||||
|
- Amount verification against configured payment
|
||||||
|
- Recipient address validation (different for native vs SPL)
|
||||||
|
- Replay attack protection (TODO: implement used transaction tracking)
|
||||||
|
- Payment method-specific validation logic
|
||||||
|
|
||||||
|
### Laconic Registry Operations
|
||||||
|
1. **ApplicationRecord**: Repository metadata and commit info
|
||||||
|
2. **Name Mappings**: LRN pointing to application record
|
||||||
|
3. **ApplicationDeploymentRequest**: Deployment configuration with payment proof
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding New Wallet Support
|
||||||
|
1. Install wallet adapter package: `npm install @solana/wallet-adapter-{wallet-name}`
|
||||||
|
2. Add to `src/components/WalletProviders.tsx` wallets array
|
||||||
|
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
|
||||||
|
- Update logic in `src/utils/solana-verify.ts`
|
||||||
|
- Consider replay protection requirements
|
||||||
|
- Test with various transaction types
|
||||||
|
- Handle both native GOR and SPL token verification
|
||||||
|
|
||||||
|
### Registry Schema Changes
|
||||||
|
- Update record structures in `src/app/api/registry/route.ts`
|
||||||
|
- Ensure compatibility with Laconic Registry schema
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Production Setup
|
||||||
|
- All environment variables must be set
|
||||||
|
- HTTPS required for wallet connections
|
||||||
|
- Consider rate limiting for API routes
|
||||||
|
- Monitor gas fees and LNT transfer costs
|
||||||
|
- Ensure both RPC endpoints are reliable
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
- Dockerfile provided in project root
|
||||||
|
- Build optimization for Next.js production
|
||||||
|
- Environment variables via docker-compose or k8s secrets
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
- **Wallet connection fails**: Check HTTPS and wallet extension
|
||||||
|
- **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
|
||||||
|
- **LNT transfer fails**: Check private key balance and permissions
|
||||||
|
- **SPL token price errors**: Verify Jupiter API connectivity
|
||||||
|
|
||||||
|
### Debug Tools
|
||||||
|
- Browser console for client-side debugging
|
||||||
|
- Server logs for API route debugging
|
||||||
|
- Solana Explorer for transaction verification
|
||||||
|
- Registry CLI for direct registry interactions
|
||||||
|
- Payment method context state in React DevTools
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- Manual testing with real wallets and tokens
|
||||||
|
- Use testnet/devnet for development
|
||||||
|
- Test transaction verification edge cases
|
||||||
|
- 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
|
||||||
|
|
||||||
|
This application references:
|
||||||
|
- `snowballtools-base/packages/backend/src/registry.ts`
|
||||||
|
- Original `hosted-frontends/deploy-atom.sh`
|
||||||
|
- Laconic Registry CLI tools
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Private keys stored securely server-side only
|
||||||
|
- Transaction replay protection implemented
|
||||||
|
- Input validation on all user inputs
|
||||||
|
- Secure RPC endpoint usage
|
||||||
|
- Wallet signature verification via adapters
|
||||||
|
- Payment method validation before processing
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- Implement transaction replay protection database
|
||||||
|
- Add support for custom deployment configurations
|
||||||
|
- Enhanced error recovery and retry logic
|
||||||
|
- Monitoring and analytics integration
|
||||||
|
- Multi-chain payment support
|
||||||
|
- Additional wallet adapter integrations
|
||||||
|
- Advanced payment method configurations
|
145
README.md
145
README.md
@ -1,13 +1,15 @@
|
|||||||
# ATOM Deploy - Laconic Registry
|
# GOR Deploy - Laconic Registry
|
||||||
|
|
||||||
A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using Keplr wallet) and paste a URL. The transaction hash and URL are used to create records in the Laconic Registry.
|
A simple Next.js frontend that allows users to pay in GOR tokens (configurable Solana SPL tokens) using Solana wallets and paste a URL. The transaction hash and URL are used to create records in the Laconic Registry.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Keplr wallet integration for ATOM payments
|
- Solana wallet integration (Phantom & Solflare) for GOR token payments
|
||||||
|
- Configurable Solana SPL token support (defaults to GOR)
|
||||||
- URL validation and submission
|
- URL validation and submission
|
||||||
- Transaction verification
|
- Solana transaction verification with replay protection
|
||||||
- Laconic Registry record creation using official `@cerc-io/registry-sdk`
|
- Laconic Registry record creation using official `@cerc-io/registry-sdk`
|
||||||
|
- LNT token transfer integration for registry payments
|
||||||
- Automatic salt addition to DNS names to prevent collisions
|
- Automatic salt addition to DNS names to prevent collisions
|
||||||
- Error handling and validation throughout the application flow
|
- Error handling and validation throughout the application flow
|
||||||
|
|
||||||
@ -15,36 +17,12 @@ A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using
|
|||||||
|
|
||||||
- Node.js 18.x or later
|
- Node.js 18.x or later
|
||||||
- npm or yarn
|
- npm or yarn
|
||||||
- Keplr wallet browser extension
|
- Solana wallet browser extension (Phantom or Solflare)
|
||||||
- Access to a Laconic Registry node
|
- Access to a Laconic Registry node
|
||||||
|
|
||||||
## Environment Variables
|
## Deploy to production
|
||||||
|
|
||||||
Copy the `.env.local.example` file to `.env.local` and fill in the required variables:
|
Follow [these steps](./deploy/README.md) to deploy this app to production
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.local.example .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
Required environment variables:
|
|
||||||
|
|
||||||
Client-side (must be prefixed with NEXT_PUBLIC_):
|
|
||||||
- `NEXT_PUBLIC_RECIPIENT_ADDRESS` - The Cosmos address that will receive ATOM payments
|
|
||||||
- `NEXT_PUBLIC_COSMOS_RPC_URL` - The RPC URL for the Cosmos blockchain (used by Keplr for transactions)
|
|
||||||
- `NEXT_PUBLIC_COSMOS_API_URL` - The REST API URL for the Cosmos blockchain (used for transaction queries)
|
|
||||||
- `NEXT_PUBLIC_COSMOS_CHAIN_ID` - The chain ID for Keplr wallet (e.g., cosmoshub-4)
|
|
||||||
- `NEXT_PUBLIC_DOMAIN_SUFFIX` - Optional suffix to append to DNS names in the UI (e.g. ".example.com")
|
|
||||||
- `NEXT_PUBLIC_EXAMPLE_URL` - Example URL to pre-fill in the URL form (e.g. "https://github.com/cerc-io/laconic-registry-cli")
|
|
||||||
|
|
||||||
Server-side:
|
|
||||||
- `REGISTRY_CHAIN_ID` - The chain ID for the Laconic Registry
|
|
||||||
- `REGISTRY_GQL_ENDPOINT` - The GraphQL endpoint for the Laconic Registry
|
|
||||||
- `REGISTRY_RPC_ENDPOINT` - The RPC endpoint for the Laconic Registry
|
|
||||||
- `REGISTRY_BOND_ID` - The bond ID to use for Laconic Registry records
|
|
||||||
- `REGISTRY_AUTHORITY` - The authority for Laconic Registry LRNs
|
|
||||||
- `REGISTRY_USER_KEY` - The private key for Laconic Registry transactions
|
|
||||||
- `APP_NAME` - The name of the application (used in record creation)
|
|
||||||
- `DEPLOYER_LRN` - The LRN of the deployer
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -54,6 +32,35 @@ npm install
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
Copy the `.env.example` file to `.env.local` and fill in the required variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Required environment variables:
|
||||||
|
|
||||||
|
Client-side (must be prefixed with NEXT_PUBLIC_):
|
||||||
|
- `NEXT_PUBLIC_SOLANA_RPC_URL` - The RPC URL for the Solana blockchain (SPL token transactions)
|
||||||
|
- `NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS` - The mint address of the SPL token to accept
|
||||||
|
- `NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL` - The token symbol to display (e.g., "GOR")
|
||||||
|
- `NEXT_PUBLIC_GORBAGANA_RPC_URL` - The RPC URL for the Gorbagana blockchain (native GOR transactions)
|
||||||
|
- `NEXT_PUBLIC_ENABLE_NATIVE_GOR_TRANSFER` - Enable native GOR token transfers (true/false)
|
||||||
|
- `NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS` - The Solana address that will receive token payments
|
||||||
|
- `NEXT_PUBLIC_EXAMPLE_URL` - Example URL to pre-fill in the URL form
|
||||||
|
- `NEXT_PUBLIC_REGISTRY_CHAIN_ID` - The laconicd chain ID for the Laconic Registry
|
||||||
|
- `NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT` - The laconicd RPC endpoint for the Laconic Registry
|
||||||
|
- `NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT` - The laconicd GraphQL endpoint for the Laconic Registry
|
||||||
|
- `NEXT_PUBLIC_ALNT_COST_LRN` - LRN for ALNT token pricing
|
||||||
|
- `NEXT_PUBLIC_DEPLOYMENT_COST_LRN` - LRN for deployment cost pricing
|
||||||
|
- `NEXT_PUBLIC_DOMAIN_SUFFIX` - Optional suffix to append to DNS names in the UI (e.g. ".example.com")
|
||||||
|
|
||||||
|
Server-side:
|
||||||
|
- `REGISTRY_BOND_ID` - The bond ID to use for Laconic Registry records
|
||||||
|
- `REGISTRY_AUTHORITY` - The authority for Laconic Registry LRNs
|
||||||
|
- `REGISTRY_USER_KEY` - The private key for Laconic Registry transactions (also used for LNT transfers)
|
||||||
|
- `DEPLOYER_LRN` - The LRN of the deployer
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
@ -74,24 +81,25 @@ npm start
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. User connects their Keplr wallet to the application
|
1. User connects their Solana wallet (Phantom or Solflare) to the application
|
||||||
2. User enters a URL they want to deploy to the Laconic Registry
|
2. User enters a URL they want to deploy to the Laconic Registry
|
||||||
3. User completes payment in ATOM to a specified address
|
3. User completes payment in GOR tokens (or configured SPL token) to a specified Solana address
|
||||||
4. The application verifies the transaction using the Cosmos RPC
|
4. The application verifies the Solana transaction with replay protection
|
||||||
5. The application calls a server-side API route which creates records in the Laconic Registry
|
5. After payment verification, the server transfers LNT tokens from a prefilled account to the service provider
|
||||||
6. The server generates a unique DNS name by adding a random salt to prevent name collisions
|
6. The application calls a server-side API route which creates records in the Laconic Registry using the LNT transfer hash
|
||||||
7. Two records are created in the Laconic Registry:
|
7. The server generates a unique DNS name by adding a random salt to prevent name collisions
|
||||||
|
8. Two records are created in the Laconic Registry:
|
||||||
- An ApplicationRecord containing metadata about the URL
|
- An ApplicationRecord containing metadata about the URL
|
||||||
- An ApplicationDeploymentRequest linking the URL, DNS, and payment transaction
|
- An ApplicationDeploymentRequest linking the URL, DNS, and payment details with external_payment metadata
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
This application uses a hybrid client/server approach:
|
This application uses a hybrid client/server approach:
|
||||||
|
|
||||||
- Client-side: Handles the user interface, Keplr wallet integration, and transaction verification
|
- Client-side: Handles the user interface, Solana wallet integration, and transaction verification
|
||||||
- Server-side: Next.js API route handles the communication with the Laconic Registry
|
- Server-side: Next.js API route handles LNT transfers and communication with the Laconic Registry
|
||||||
|
|
||||||
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience.
|
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience. The dual-payment system (Solana → LNT → Registry) enables cross-chain payment acceptance.
|
||||||
|
|
||||||
### Resource Name Formats
|
### Resource Name Formats
|
||||||
|
|
||||||
@ -115,7 +123,7 @@ The Laconic Resource Names (LRNs) are generated with the following format:
|
|||||||
lrn://{authority}/applications/{app-name}-{short-commit-hash}-{random-salt}
|
lrn://{authority}/applications/{app-name}-{short-commit-hash}-{random-salt}
|
||||||
```
|
```
|
||||||
|
|
||||||
For example: `lrn://atom/applications/github-abc123-xyz789`
|
For example: `lrn://gor/applications/github-abc123-xyz789`
|
||||||
|
|
||||||
Including the commit hash and salt in the LRN ensures that each application record has a unique identifier, consistently matching the DNS naming pattern.
|
Including the commit hash and salt in the LRN ensures that each application record has a unique identifier, consistently matching the DNS naming pattern.
|
||||||
|
|
||||||
@ -123,59 +131,24 @@ Including the commit hash and salt in the LRN ensures that each application reco
|
|||||||
|
|
||||||
This application was built with reference to:
|
This application was built with reference to:
|
||||||
- `snowballtools-base/packages/backend/src/registry.ts`
|
- `snowballtools-base/packages/backend/src/registry.ts`
|
||||||
- `hosted-frontends/deploy-atom.sh`
|
- Original `hosted-frontends/deploy-atom.sh` (adapted for Solana/GOR)
|
||||||
|
|
||||||
## Deployment to Production
|
|
||||||
|
|
||||||
To deploy this application to production, follow these steps:
|
|
||||||
|
|
||||||
1. Clone the repository
|
|
||||||
2. Install dependencies: `npm install`
|
|
||||||
3. Create a production build: `npm run build`
|
|
||||||
4. Set up all required environment variables in your production environment
|
|
||||||
5. Start the production server: `npm start`
|
|
||||||
|
|
||||||
For containerized deployments, you can use the following Dockerfile:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM node:18-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Build and run the Docker container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t atom-deploy .
|
|
||||||
docker run -p 3000:3000 --env-file .env.production atom-deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
- You may see a deprecated Buffer() warning during build. This comes from dependencies in the registry-sdk. This doesn't affect functionality.
|
- You may see a deprecated Buffer() warning during build. This comes from dependencies in the registry-sdk. This doesn't affect functionality.
|
||||||
- If using a custom Cosmos chain, ensure that your RPC endpoint supports CORS for client-side requests.
|
- Ensure that your Solana RPC endpoint supports CORS for client-side requests.
|
||||||
- The Keplr wallet integration requires HTTPS in production environments.
|
- Solana wallet integrations require HTTPS in production environments.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Keplr Wallet Issues
|
### Solana Wallet Issues
|
||||||
|
|
||||||
- **Keplr not detecting**: Install the Keplr browser extension and refresh the page.
|
- **Wallet not detecting**: Install the Phantom or Solflare browser extension and refresh the page.
|
||||||
- **Chain not found in Keplr**: The application will attempt to suggest the chain to Keplr, but if that fails, you may need to manually add the chain in your Keplr wallet settings.
|
- **Connection issues**: Ensure the wallet is unlocked and try refreshing the page.
|
||||||
|
- **Transaction failures**: Check that you have sufficient SOL for transaction fees and enough tokens for the payment.
|
||||||
|
|
||||||
### Laconic Registry Issues
|
### Laconic Registry Issues
|
||||||
|
|
||||||
- **Failed to create record**: Check that your REGISTRY_USER_KEY and REGISTRY_BOND_ID are correctly set.
|
- **Failed to create record**: Check that your REGISTRY_USER_KEY and REGISTRY_BOND_ID are correctly set.
|
||||||
- **Transaction verification errors**: Ensure your COSMOS_RPC_URL and COSMOS_API_URL are accessible and return correct transaction data.
|
- **LNT transfer errors**: Ensure your REGISTRY_USER_KEY has sufficient LNT balance.
|
||||||
|
- **Transaction verification errors**: Ensure your SOLANA_RPC_URL is accessible and returns correct transaction data.
|
||||||
|
10
deploy/.registry.env.example
Normal file
10
deploy/.registry.env.example
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# ENV for registry operations
|
||||||
|
|
||||||
|
# Bond to use
|
||||||
|
REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
|
||||||
|
|
||||||
|
# Target deployer LRN
|
||||||
|
DEPLOYER_LRN=lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
||||||
|
|
||||||
|
# Authority to deploy the app under
|
||||||
|
AUTHORITY=laconic-deploy
|
40
deploy/Dockerfile
Normal file
40
deploy/Dockerfile
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
ARG VARIANT=20-bullseye
|
||||||
|
FROM node:${VARIANT}
|
||||||
|
|
||||||
|
ARG USERNAME=node
|
||||||
|
ARG NPM_GLOBAL=/usr/local/share/npm-global
|
||||||
|
|
||||||
|
# Add NPM global to PATH.
|
||||||
|
ENV PATH=${NPM_GLOBAL}/bin:${PATH}
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Configure global npm install location, use group to adapt to UID/GID changes
|
||||||
|
if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \
|
||||||
|
&& usermod -a -G npm ${USERNAME} \
|
||||||
|
&& umask 0002 \
|
||||||
|
&& mkdir -p ${NPM_GLOBAL} \
|
||||||
|
&& touch /usr/local/etc/npmrc \
|
||||||
|
&& chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \
|
||||||
|
&& chmod g+s ${NPM_GLOBAL} \
|
||||||
|
&& npm config -g set prefix ${NPM_GLOBAL} \
|
||||||
|
&& su ${USERNAME} -c "npm config -g set prefix ${NPM_GLOBAL}" \
|
||||||
|
# Install eslint
|
||||||
|
&& su ${USERNAME} -c "umask 0002 && npm install -g eslint" \
|
||||||
|
&& npm cache clean --force > /dev/null 2>&1
|
||||||
|
|
||||||
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends jq bash
|
||||||
|
|
||||||
|
# laconic-so
|
||||||
|
RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/latest/laconic-so && \
|
||||||
|
chmod +x ./laconic-so && \
|
||||||
|
mv ./laconic-so /usr/bin/laconic-so
|
||||||
|
|
||||||
|
# Configure the npm registry
|
||||||
|
RUN npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
|
||||||
|
|
||||||
|
# DEBUG, remove
|
||||||
|
RUN yarn info @cerc-io/laconic-registry-cli
|
||||||
|
|
||||||
|
# Globally install the cli package
|
||||||
|
RUN yarn global add @cerc-io/laconic-registry-cli
|
110
deploy/README.md
Normal file
110
deploy/README.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Deploy
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### gor-deploy
|
||||||
|
|
||||||
|
- Clone the repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@git.vdb.to:LaconicNetwork/gor-deploy.git
|
||||||
|
cd gor-deploy/deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build registry CLI image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t cerc/laconic-registry-cli .
|
||||||
|
|
||||||
|
# Builds image cerc/laconic-registry-cli:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure `userKey` and `bondId` in the [registry CLI config](./config.yml):
|
||||||
|
|
||||||
|
- User key should be of the account that owns the `laconic-deploy` authority (owner account address: `laconic1kwx2jm6vscz38qlyujvq6msujmk8l3zangqahs`)
|
||||||
|
- The bond should also be owned by same user (owned bond's ID: `5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2`)
|
||||||
|
- If the authority is not available, follow [these steps to reserve a new authority](./publish-pricing.md#reserve-a-new-authority-optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
- This project requires pricing records for cost of deployment and cost of alnt to be published
|
||||||
|
|
||||||
|
- Check if these records are available by running following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if `lrn://laconic/pricing/webapp-deployment` is available
|
||||||
|
./laconic-cli.sh name resolve lrn://laconic/pricing/webapp-deployment
|
||||||
|
|
||||||
|
# Check if `lrn://laconic/pricing/alnt` is available
|
||||||
|
./laconic-cli.sh name resolve lrn://laconic/pricing/alnt
|
||||||
|
```
|
||||||
|
|
||||||
|
- If these records are not available, [follow these steps to publish them](./publish-pricing.md)
|
||||||
|
|
||||||
|
- Add configuration for registry operations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .registry.env.example .registry.env
|
||||||
|
|
||||||
|
# Update values if required
|
||||||
|
nano .registry.env
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add configuration for the app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://git.vdb.to/LaconicNetwork/gor-deploy/raw/branch/main/.env.example -o .app.env
|
||||||
|
|
||||||
|
# Fill in the required values
|
||||||
|
nano .app.env
|
||||||
|
```
|
||||||
|
|
||||||
|
- Required environment variables:
|
||||||
|
|
||||||
|
Client-side (must be prefixed with NEXT_PUBLIC_):
|
||||||
|
- `NEXT_PUBLIC_SOLANA_RPC_URL` - The RPC URL for the Solana blockchain (SPL token transactions)
|
||||||
|
- `NEXT_PUBLIC_GORBAGANA_RPC_URL` - The RPC URL for the Gorbagana blockchain (native GOR transactions)
|
||||||
|
- `NEXT_PUBLIC_ENABLE_NATIVE_GOR_TRANSFER` - Enable native GOR token transfers (true/false)
|
||||||
|
- `NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS` - The Solana address that will receive token payments
|
||||||
|
- `NEXT_PUBLIC_ALNT_COST_LRN` - LRN for ALNT token pricing
|
||||||
|
- `NEXT_PUBLIC_DEPLOYMENT_COST_LRN` - LRN for deployment cost pricing
|
||||||
|
- `NEXT_PUBLIC_DOMAIN_SUFFIX` - Suffix to append to DNS names in the UI (e.g. ".example.com")
|
||||||
|
|
||||||
|
Server-side:
|
||||||
|
- `REGISTRY_BOND_ID` - The bond ID to use for Laconic Registry records
|
||||||
|
- `REGISTRY_AUTHORITY` - The authority for Laconic Registry LRNs
|
||||||
|
- `REGISTRY_USER_KEY` - The private key of account which will publish records
|
||||||
|
- The account should own the `laconic-deploy` authority which is set in `REGISTRY_AUTHORITY`
|
||||||
|
- `DEPLOYER_LRN` - The LRN of the deployer
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
- Deploy `gor-deploy` App:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In gor-deploy/deploy dir
|
||||||
|
docker run -it \
|
||||||
|
-v ./:/app/deploy -w /app/deploy \
|
||||||
|
-e DEPLOYMENT_DNS=gor-deploy.laconic.com \
|
||||||
|
cerc/laconic-registry-cli:latest \
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check deployment logs on deployer UI: <https://webapp-deployer-ui.apps.vaasl.io/>
|
||||||
|
|
||||||
|
- Visit deployed app: <https://gor-deploy.apps.vaasl.io>
|
||||||
|
|
||||||
|
### Remove deployment
|
||||||
|
|
||||||
|
- Remove deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In gor-deploy/deploy dir
|
||||||
|
docker run -it \
|
||||||
|
-v ./:/app/deploy -w /app/deploy \
|
||||||
|
-e DEPLOYMENT_RECORD_ID=<deploment-record-id-to-be-removed> \
|
||||||
|
cerc/laconic-registry-cli:latest \
|
||||||
|
./remove-deployment.sh
|
||||||
|
```
|
9
deploy/config.yml
Normal file
9
deploy/config.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Registry CLI config
|
||||||
|
services:
|
||||||
|
registry:
|
||||||
|
rpcEndpoint: 'https://laconicd-mainnet-1.laconic.com'
|
||||||
|
gqlEndpoint: 'https://laconicd-mainnet-1.laconic.com/api'
|
||||||
|
userKey:
|
||||||
|
bondId: 5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
|
||||||
|
chainId: laconic-mainnet
|
||||||
|
gasPrice: 0.001alnt
|
128
deploy/deploy.sh
Executable file
128
deploy/deploy.sh
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Fail on error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
source .registry.env
|
||||||
|
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||||
|
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||||
|
echo "Using AUTHORITY: $AUTHORITY"
|
||||||
|
|
||||||
|
# Repository URL
|
||||||
|
REPO_URL="https://git.vdb.to/LaconicNetwork/gor-deploy"
|
||||||
|
|
||||||
|
# Get the latest commit hash for a branch
|
||||||
|
BRANCH_NAME="main"
|
||||||
|
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||||
|
|
||||||
|
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version)
|
||||||
|
|
||||||
|
APP_NAME=gor-deploy
|
||||||
|
|
||||||
|
echo "Repo: ${REPO_URL}"
|
||||||
|
echo "Latest hash: ${LATEST_HASH}"
|
||||||
|
echo "App version: ${PACKAGE_VERSION}"
|
||||||
|
echo "Deployment DNS: ${DEPLOYMENT_DNS}"
|
||||||
|
|
||||||
|
# Current date and time for note
|
||||||
|
CURRENT_DATE_TIME=$(date -u)
|
||||||
|
|
||||||
|
CONFIG_FILE=config.yml
|
||||||
|
|
||||||
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
|
# Get latest version from registry and increment application-record version
|
||||||
|
NEW_APPLICATION_VERSION=$(laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||||
|
|
||||||
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
|
# Set application-record version if no previous records were found
|
||||||
|
NEW_APPLICATION_VERSION=0.0.1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate application-record.yml with incremented version
|
||||||
|
mkdir -p records
|
||||||
|
RECORD_FILE=./records/application-record.yml
|
||||||
|
|
||||||
|
cat >$RECORD_FILE <<EOF
|
||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: $NEW_APPLICATION_VERSION
|
||||||
|
repository_ref: $LATEST_HASH
|
||||||
|
repository: ["$REPO_URL"]
|
||||||
|
app_type: webapp
|
||||||
|
name: $APP_NAME
|
||||||
|
app_version: $PACKAGE_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Application record generated successfully: $RECORD_FILE"
|
||||||
|
|
||||||
|
# Publish ApplicationRecord
|
||||||
|
publish_response=$(laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to publish record"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||||
|
echo "ApplicationRecord published, setting names next"
|
||||||
|
echo $RECORD_ID
|
||||||
|
|
||||||
|
# Set name to record
|
||||||
|
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/$APP_NAME"
|
||||||
|
|
||||||
|
name1="$REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||||
|
sleep 2
|
||||||
|
laconic -c $CONFIG_FILE registry name set "$name1" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$name1 set for ApplicationRecord"
|
||||||
|
|
||||||
|
name2="$REGISTRY_APP_LRN@${LATEST_HASH}"
|
||||||
|
sleep 2
|
||||||
|
laconic -c $CONFIG_FILE registry name set "$name2" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set hash"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$name2 set for ApplicationRecord"
|
||||||
|
|
||||||
|
name3="$REGISTRY_APP_LRN"
|
||||||
|
sleep 2
|
||||||
|
# Set name if latest release
|
||||||
|
laconic -c $CONFIG_FILE registry name set "$name3" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set release"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$name3 set for ApplicationRecord"
|
||||||
|
|
||||||
|
# Check if record found for REGISTRY_APP_LRN
|
||||||
|
query_response=$(laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query name"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Name resolution successful"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
echo "Requesting a webapp deployment for $name2, using deployer $DEPLOYER_LRN"
|
||||||
|
laconic-so request-webapp-deployment \
|
||||||
|
--laconic-config $CONFIG_FILE \
|
||||||
|
--deployer $DEPLOYER_LRN \
|
||||||
|
--app $name2 \
|
||||||
|
--env-file ./.app.env \
|
||||||
|
--dns $DEPLOYMENT_DNS \
|
||||||
|
--make-payment auto
|
||||||
|
|
||||||
|
echo "Done"
|
50
deploy/laconic-cli.sh
Executable file
50
deploy/laconic-cli.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Laconic Registry CLI Docker wrapper script
|
||||||
|
# This script wraps the Docker command to run laconic registry CLI commands
|
||||||
|
# Run this script from the deploy directory
|
||||||
|
|
||||||
|
# Check if docker is available
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "Error: Docker is not installed or not in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the cerc/laconic-registry-cli image exists
|
||||||
|
if ! docker image inspect cerc/laconic-registry-cli &> /dev/null; then
|
||||||
|
echo "Error: cerc/laconic-registry-cli Docker image not found"
|
||||||
|
echo "Please build the image first: docker build -t cerc/laconic-registry-cli ."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current directory (should be deploy directory)
|
||||||
|
CURRENT_DIR="$(pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$CURRENT_DIR")"
|
||||||
|
|
||||||
|
# Verify we're in the deploy directory
|
||||||
|
if [ ! -f "config.yml" ] || [ ! -f "laconic-cli.sh" ]; then
|
||||||
|
echo "Error: This script must be run from the deploy directory"
|
||||||
|
echo "Current directory: $CURRENT_DIR"
|
||||||
|
echo "Please cd to the deploy directory and run: ./laconic-cli.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up volume mounts
|
||||||
|
DEPLOY_MOUNT="-v $CURRENT_DIR:/app/deploy"
|
||||||
|
OUT_MOUNT=""
|
||||||
|
|
||||||
|
# Create out directory if it doesn't exist and always mount it
|
||||||
|
if [ ! -d "out" ]; then
|
||||||
|
mkdir -p "out"
|
||||||
|
fi
|
||||||
|
OUT_MOUNT="-v $CURRENT_DIR/out:/app/out"
|
||||||
|
|
||||||
|
# Run the Docker command with processed arguments
|
||||||
|
docker run --rm \
|
||||||
|
--add-host=host.docker.internal:host-gateway \
|
||||||
|
$DEPLOY_MOUNT \
|
||||||
|
$OUT_MOUNT \
|
||||||
|
-w /app/deploy \
|
||||||
|
cerc/laconic-registry-cli \
|
||||||
|
laconic registry -c config.yml \
|
||||||
|
"$@"
|
147
deploy/publish-pricing.md
Normal file
147
deploy/publish-pricing.md
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# publish-pricing
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- Clone the repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@git.vdb.to:LaconicNetwork/gor-deploy.git
|
||||||
|
cd gor-deploy/deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build the Docker container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t cerc/laconic-registry-cli .
|
||||||
|
|
||||||
|
# Builds image cerc/laconic-registry-cli:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure `userKey` in the [registry CLI config](./config.yml):
|
||||||
|
|
||||||
|
NOTE: The `laconic` authority is required to set the published record names so the user key should be of the account that owns the `laconic` authority (owner account address: `laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef`). If you don't have an authority check [steps to reserve a new authority](#reserve-a-new-authority-optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publish Record and Set Record Name
|
||||||
|
|
||||||
|
Publishing record requires a bond with enough funds as rent is taken for each record from the bond every year
|
||||||
|
|
||||||
|
The rent amount taken is `1000000alnt` so maintaining a bond with about 10x the rent amount i.e `10000000alnt` is recommended
|
||||||
|
|
||||||
|
Make sure, the record names are under `laconic` authority
|
||||||
|
|
||||||
|
### Publish Record for Cost of alnt
|
||||||
|
|
||||||
|
- alnt price calculation (reference: <https://store.laconic.com>):
|
||||||
|
- Cost of 1 deployment is `12960` in terms of `alnt` or `5` in terms of `USD`
|
||||||
|
- Hence, cost of 1 `alnt` comes out be `0.000386 USD` rounded off to 6 decimals
|
||||||
|
|
||||||
|
- Publish the record:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh record publish --filename records/alnt-pricing.yml --bond-id <bond-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Get record info:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh record get --id <record-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Set record name for cost of alnt record:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh name set lrn://laconic/pricing/alnt <record-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publish Record for Cost of Deployment
|
||||||
|
|
||||||
|
- Publish the record:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh record publish --filename records/webapp-deployment-pricing.yml --bond-id <bond-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Get record info:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh record get --id <record-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Set record name for cost of deployment record
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh name set lrn://laconic/pricing/webapp-deployment <record-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Now you should be able to use these records in the app
|
||||||
|
|
||||||
|
- You can now continue with [steps to deploy the app](./README.md)
|
||||||
|
|
||||||
|
## Create Bond (optional)
|
||||||
|
|
||||||
|
- Create bond:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh bond create --type alnt --quantity 10000000
|
||||||
|
```
|
||||||
|
|
||||||
|
- Get bond info:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh bond get --id <bond-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reserve a New Authority (optional)
|
||||||
|
|
||||||
|
Below steps are used to reserve `laconic` authority
|
||||||
|
|
||||||
|
- Reserve authority:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh authority reserve laconic
|
||||||
|
```
|
||||||
|
|
||||||
|
- After reserving authority, commit phase begins which lasts for 1 minute so please commit the bid following below steps within that time period
|
||||||
|
|
||||||
|
- Check authority info:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh authority whois laconic
|
||||||
|
```
|
||||||
|
|
||||||
|
- Note down auction ID from authority info as it is required in next steps
|
||||||
|
|
||||||
|
- Get auction info:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh auction get <auction-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Commit an auction bid:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 5000000 alnt is the minimum bid amount for authority auction
|
||||||
|
|
||||||
|
./laconic-cli.sh auction bid commit <auction-id> 5000000 alnt
|
||||||
|
|
||||||
|
# Example file path inside container
|
||||||
|
Reveal file: /app/deploy/out/bafyreiay2rccax64yn4ljhvzvm3jkbebvzheyucuma5jlbpzpzd5i5gjuy.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- The reveal phase starts as soon as commit phase ends and lasts for 1 minute so please reveal the bid within this time period
|
||||||
|
|
||||||
|
- Reveal bid:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh auction bid reveal <auction-id> /app/deploy/out/<reaveal-file>.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- Set authority bond after winning auction as it is required to use the published authority:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./laconic-cli.sh authority bond set laconic <bond-id>
|
||||||
|
```
|
0
deploy/records/.gitkeep
Normal file
0
deploy/records/.gitkeep
Normal file
6
deploy/records/alnt-pricing.yml
Normal file
6
deploy/records/alnt-pricing.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
record:
|
||||||
|
type: PricingRecord
|
||||||
|
for: "alnt"
|
||||||
|
amount: "0.000386"
|
||||||
|
currency: "USD"
|
||||||
|
version: 1.0.0
|
8
deploy/records/application-record.yml
Normal file
8
deploy/records/application-record.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: 0.0.4
|
||||||
|
repository_ref: bc0d10d4c38d2d20de88ac8352ab8571bbbffef4
|
||||||
|
repository: ["https://git.vdb.to/LaconicNetwork/gor-deploy"]
|
||||||
|
app_type: webapp
|
||||||
|
name: gor-deploy
|
||||||
|
app_version: 0.1.0
|
6
deploy/records/webapp-deployment-pricing.yml
Normal file
6
deploy/records/webapp-deployment-pricing.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
record:
|
||||||
|
type: PricingRecord
|
||||||
|
for: "webapp-deployment"
|
||||||
|
amount: "12960"
|
||||||
|
currency: "alnt"
|
||||||
|
version: 1.0.0
|
63
deploy/remove-deployment.sh
Executable file
63
deploy/remove-deployment.sh
Executable file
@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ -z $DEPLOYMENT_RECORD_ID ]]; then
|
||||||
|
echo "Error: please pass the deployment record ID" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .registry.env
|
||||||
|
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||||
|
|
||||||
|
echo "Deployment record ID: $DEPLOYMENT_RECORD_ID"
|
||||||
|
|
||||||
|
# Generate application-deployment-removal-request.yml
|
||||||
|
REMOVAL_REQUEST_RECORD_FILE=./records/application-deployment-removal-request.yml
|
||||||
|
|
||||||
|
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||||
|
record:
|
||||||
|
deployer: $DEPLOYER_LRN
|
||||||
|
deployment: $DEPLOYMENT_RECORD_ID
|
||||||
|
type: ApplicationDeploymentRemovalRequest
|
||||||
|
version: 1.0.0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
CONFIG_FILE=config.yml
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
REMOVAL_REQUEST_ID=$(laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationDeploymentRemovalRequest published"
|
||||||
|
echo $REMOVAL_REQUEST_ID
|
||||||
|
|
||||||
|
# Deployment checks
|
||||||
|
RETRY_INTERVAL=30
|
||||||
|
MAX_RETRIES=20
|
||||||
|
|
||||||
|
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||||
|
retry_count=0
|
||||||
|
while true; do
|
||||||
|
removal_records_response=$(laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||||
|
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||||
|
|
||||||
|
# Check if number of records returned is 0
|
||||||
|
if [ $len_removal_records -eq 0 ]; then
|
||||||
|
# Check if retries are exhausted
|
||||||
|
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||||
|
echo "Retries exhausted"
|
||||||
|
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||||
|
sleep $RETRY_INTERVAL
|
||||||
|
retry_count=$((retry_count+1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ApplicationDeploymentRemovalRecord found"
|
||||||
|
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||||
|
echo $REMOVAL_RECORD_ID
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deployment removal successful"
|
@ -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;
|
||||||
|
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
@ -1,6 +1,6 @@
|
|||||||
import type { NextConfig } from "next";
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
typescript: {
|
typescript: {
|
||||||
// !! WARN !!
|
// !! WARN !!
|
5205
package-lock.json
generated
5205
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,18 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "atom-deploy",
|
"name": "gor-deploy",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"laconic": "laconic registry -c deploy/config.yml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cerc-io/registry-sdk": "^0.2.11",
|
"@cerc-io/registry-sdk": "^0.2.11",
|
||||||
"@cosmjs/stargate": "^0.32.3",
|
"@cosmjs/stargate": "^0.32.3",
|
||||||
"@keplr-wallet/types": "^0.12.71",
|
"@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",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"bn.js": "^5.2.2",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
@ -20,10 +29,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@cerc-io/laconic-registry-cli": "^0.2.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"
|
||||||
|
@ -1,120 +1,35 @@
|
|||||||
|
import BN from 'bn.js';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { Account, Registry, parseGasAndFees } from '@cerc-io/registry-sdk';
|
|
||||||
import { GasPrice } from '@cosmjs/stargate';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import { Connection, ParsedTransactionWithMeta } from '@solana/web3.js';
|
||||||
|
|
||||||
|
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
||||||
|
import { transferLNTTokens } from '@/services/laconic-transfer';
|
||||||
|
import { getRegistry, getRegistryConfig } from '@/config';
|
||||||
|
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||||
|
import { SOLANA_GOR_MINT_ADDRESS } from '@/constants/payments';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
import { getCostOfDeployment } from '@/services/registry';
|
||||||
|
import { IS_NAT_GOR_TRANSFER_ENABLED } from '@/utils/gorbagana';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Allow 20% slippage due to price fluctuations
|
||||||
|
const ALLOWED_SLIPPAGE_FACTOR = 0.2
|
||||||
|
|
||||||
|
// Use CAIP convention for chain ID: namespace + reference
|
||||||
|
const SOLANA_CHAIN_ID = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; // Solana mainnet
|
||||||
|
const GORBAGANA_CHAIN_ID = 'gorbagana:533uBE9RRquhTBqEX58oV52FdTTsReMd' // Gorbagana chain (first 32 characters of gorbagana genesis hash. Following solana CAIP chain ID pattern)
|
||||||
|
|
||||||
// Sleep helper function
|
// Sleep helper function
|
||||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
// ATOM payment verification function
|
|
||||||
const verifyAtomPayment = async (txHash: string): Promise<{
|
|
||||||
valid: boolean,
|
|
||||||
reason?: string,
|
|
||||||
amount?: string,
|
|
||||||
sender?: string
|
|
||||||
}> => {
|
|
||||||
try {
|
|
||||||
const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL;
|
|
||||||
const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS;
|
|
||||||
const minPaymentUAtom = '100000'; // 0.1 ATOM in uatom
|
|
||||||
|
|
||||||
if (!apiEndpoint) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'ATOM API endpoint not configured'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recipientAddress) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'ATOM recipient address not configured'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch transaction from the ATOM API endpoint
|
|
||||||
const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`);
|
|
||||||
|
|
||||||
if (!response.data || !response.data.tx || !response.data.tx_response) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: 'Invalid transaction data from API endpoint'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if transaction was successful
|
|
||||||
const txResponse = response.data.tx_response;
|
|
||||||
if (txResponse.code !== 0) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: `Transaction failed with code ${txResponse.code}: ${txResponse.raw_log}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check transaction timestamp (5-minute window)
|
|
||||||
const txTimestamp = new Date(txResponse.timestamp);
|
|
||||||
const now = new Date();
|
|
||||||
const timeDiffMs = now.getTime() - txTimestamp.getTime();
|
|
||||||
const timeWindowMs = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
if (timeDiffMs > timeWindowMs) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: `Transaction is older than 5 minutes (${Math.round(timeDiffMs / 60000)} minutes old)`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the payment details
|
|
||||||
const tx = response.data.tx;
|
|
||||||
let foundValidPayment = false;
|
|
||||||
let paymentAmountUAtom = '';
|
|
||||||
let sender = '';
|
|
||||||
|
|
||||||
// Get the sender address from the first signer
|
|
||||||
if (tx.auth_info && tx.auth_info.signer_infos && tx.auth_info.signer_infos.length > 0) {
|
|
||||||
sender = tx.auth_info.signer_infos[0].public_key.address || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the send message in the transaction
|
|
||||||
for (const msg of tx.body.messages) {
|
|
||||||
if (msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') {
|
|
||||||
if (msg.to_address === recipientAddress) {
|
|
||||||
for (const coin of msg.amount) {
|
|
||||||
if (coin.denom === 'uatom') {
|
|
||||||
// Get the amount in uatom
|
|
||||||
paymentAmountUAtom = coin.amount;
|
|
||||||
|
|
||||||
if (parseInt(paymentAmountUAtom) >= parseInt(minPaymentUAtom)) {
|
|
||||||
foundValidPayment = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundValidPayment) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: `Payment amount (${paymentAmountUAtom || '0'}uatom) is less than required (${minPaymentUAtom}uatom) or not sent to the correct address (${recipientAddress})`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
amount: `${paymentAmountUAtom}uatom`,
|
|
||||||
sender
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying ATOM payment:', error);
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: `Failed to verify transaction: ${error.message || 'Unknown error'}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract repo name from URL
|
// Extract repo name from URL
|
||||||
const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, provider: string } => {
|
const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, provider: string } => {
|
||||||
try {
|
try {
|
||||||
@ -202,7 +117,7 @@ const fetchLatestCommitHash = async (repoUrl: string, provider: string): Promise
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Registry transaction retry helper
|
// Registry transaction retry helper
|
||||||
const registryTransactionWithRetry = async (
|
export const registryTransactionWithRetry = async (
|
||||||
txFn: () => Promise<unknown>,
|
txFn: () => Promise<unknown>,
|
||||||
maxRetries = 3,
|
maxRetries = 3,
|
||||||
delay = 1000
|
delay = 1000
|
||||||
@ -225,56 +140,163 @@ const registryTransactionWithRetry = async (
|
|||||||
throw lastError;
|
throw lastError;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
// First check if the request body is valid JSON
|
// First check if the request body is valid JSON
|
||||||
let url, txHash;
|
let url, txHash, senderPublicKey, paymentMethod;
|
||||||
|
let connection: Connection;
|
||||||
|
let parsedTx: ParsedTransactionWithMeta | null;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
if (!url || !txHash) {
|
// Get the appropriate connection based on payment method
|
||||||
|
connection = getConnection(paymentMethod);
|
||||||
|
|
||||||
|
const tx = await connection.getParsedTransaction(txHash, 'confirmed');
|
||||||
|
if (!tx) {
|
||||||
|
console.error("Transaction not found.");
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Missing required fields: url and txHash are required'
|
message: 'Invalid tx hash'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedTx = tx;
|
||||||
|
|
||||||
|
const signerKeys = tx.transaction.message.accountKeys
|
||||||
|
.filter(k => k.signer)
|
||||||
|
.map(k => k.pubkey.toBase58());
|
||||||
|
|
||||||
|
senderPublicKey = signerKeys[0];
|
||||||
|
|
||||||
|
if (!url || !txHash || !paymentMethod) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'error',
|
||||||
|
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) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Invalid JSON in request body'
|
message: 'Invalid JSON in request body'
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, verify the ATOM payment before doing anything else
|
// Verify Solana payment
|
||||||
console.log('Step 0: Verifying ATOM payment...');
|
console.log('Step 0: Verifying Solana token payment...');
|
||||||
const paymentVerificationResult = await verifyAtomPayment(txHash);
|
let requiredTokenInfo: RequiredTokenInfo;
|
||||||
|
const targetUsdAmount = await getCostOfDeployment();
|
||||||
|
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||||
|
|
||||||
if (!paymentVerificationResult.valid) {
|
try {
|
||||||
console.error('ATOM payment verification failed:', paymentVerificationResult.reason);
|
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, SOLANA_GOR_MINT_ADDRESS);
|
||||||
|
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(targetUsdAmount, mintAddress);
|
||||||
|
} else {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `Payment verification failed: ${paymentVerificationResult.reason}`
|
message: 'Unsupported payment method'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calculating expected amount:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Unable to verify payment due to price calculation error'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
|
||||||
|
const expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
||||||
|
|
||||||
|
if (!parsedTx) {
|
||||||
|
throw new Error(`Unable to find the tx with hash: ${txHash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const solanaPaymentResult = await verifyUnusedSolanaPayment(
|
||||||
|
connection,
|
||||||
|
parsedTx,
|
||||||
|
new BN(expectedTokenAmount),
|
||||||
|
paymentMethod,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!solanaPaymentResult.valid) {
|
||||||
|
console.error(`Solana ${paymentMethod} payment verification failed:`, solanaPaymentResult.reason);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'error',
|
||||||
|
message: `Payment verification failed: ${solanaPaymentResult.reason}`
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('ATOM payment verified successfully:', {
|
console.log(`Solana ${paymentMethod} payment verified successfully:`, {
|
||||||
amount: paymentVerificationResult.amount,
|
amount: solanaPaymentResult.amount,
|
||||||
sender: paymentVerificationResult.sender
|
sender: solanaPaymentResult.sender
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate required environment variables
|
// Perform LNT transfer after payment verification
|
||||||
|
console.log('Step 0.5: Performing LNT transfer from prefilled account to service provider...');
|
||||||
|
const lntTransferResult = await transferLNTTokens();
|
||||||
|
|
||||||
|
if (!lntTransferResult.success) {
|
||||||
|
console.error('LNT transfer failed:', lntTransferResult.error);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'error',
|
||||||
|
message: `LNT transfer failed: ${lntTransferResult.error}`
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('LNT transfer completed:', lntTransferResult.transactionHash);
|
||||||
|
const laconicTxHash = lntTransferResult.transactionHash!; // Use LNT transfer hash for registry
|
||||||
|
|
||||||
|
// Validate required environment variables for Solana payments
|
||||||
const requiredEnvVars = [
|
const requiredEnvVars = [
|
||||||
'REGISTRY_CHAIN_ID',
|
'NEXT_PUBLIC_REGISTRY_CHAIN_ID',
|
||||||
'REGISTRY_GQL_ENDPOINT',
|
'NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT',
|
||||||
'REGISTRY_RPC_ENDPOINT',
|
'NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT',
|
||||||
'REGISTRY_BOND_ID',
|
'REGISTRY_BOND_ID',
|
||||||
'REGISTRY_AUTHORITY',
|
'REGISTRY_AUTHORITY',
|
||||||
'REGISTRY_USER_KEY',
|
'REGISTRY_USER_KEY', // This is the same as the prefilled account for LNT transfers
|
||||||
'DEPLOYER_LRN',
|
'DEPLOYER_LRN',
|
||||||
'NEXT_PUBLIC_RECIPIENT_ADDRESS',
|
'NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS',
|
||||||
'NEXT_PUBLIC_COSMOS_API_URL'
|
'NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const envVar of requiredEnvVars) {
|
for (const envVar of requiredEnvVars) {
|
||||||
@ -302,17 +324,9 @@ export async function POST(request: NextRequest) {
|
|||||||
// Sanitize the app name to ensure it's DNS-compatible (only alphanumeric and dashes)
|
// Sanitize the app name to ensure it's DNS-compatible (only alphanumeric and dashes)
|
||||||
const sanitizedAppName = appName.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
const sanitizedAppName = appName.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
||||||
|
|
||||||
// Generate a random salt (6 alphanumeric characters) to prevent name collisions
|
// Create DNS name in format: app_name-shortcommithash
|
||||||
const generateSalt = (): string => {
|
const dnsName = `${sanitizedAppName}-${shortHash}`;
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
console.log(`DNS name: ${dnsName} (sanitized from: ${appName})`);
|
||||||
return Array.from({ length: 6 }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
|
|
||||||
};
|
|
||||||
const salt = generateSalt();
|
|
||||||
console.log(`Generated salt: ${salt}`);
|
|
||||||
|
|
||||||
// Create DNS name in format: app_name-shortcommithash-salt
|
|
||||||
const dnsName = `${sanitizedAppName}-${shortHash}-${salt}`;
|
|
||||||
console.log(`DNS name with salt: ${dnsName} (sanitized from: ${appName})`);
|
|
||||||
|
|
||||||
// Ensure the DNS name doesn't have consecutive dashes or start/end with a dash
|
// Ensure the DNS name doesn't have consecutive dashes or start/end with a dash
|
||||||
let cleanDnsName = dnsName
|
let cleanDnsName = dnsName
|
||||||
@ -321,10 +335,9 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// Ensure DNS name is valid (63 chars max per label, all lowercase, starts with a letter)
|
// Ensure DNS name is valid (63 chars max per label, all lowercase, starts with a letter)
|
||||||
if (cleanDnsName.length > 63) {
|
if (cleanDnsName.length > 63) {
|
||||||
// If too long, truncate but preserve both the commit hash and salt parts
|
// If too long, truncate but preserve both the commit hash
|
||||||
const suffixPart = `-${shortHash}-${salt}`;
|
const maxAppNameLength = 63 - shortHash.length;
|
||||||
const maxAppNameLength = 63 - suffixPart.length;
|
cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + shortHash;
|
||||||
cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + suffixPart;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the DNS name ended up empty (unlikely) or doesn't start with a letter (possible),
|
// If the DNS name ended up empty (unlikely) or doesn't start with a letter (possible),
|
||||||
@ -333,22 +346,10 @@ export async function POST(request: NextRequest) {
|
|||||||
cleanDnsName = `app-${cleanDnsName}`;
|
cleanDnsName = `app-${cleanDnsName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Final DNS name with salt: ${cleanDnsName}`);
|
console.log(`Final DNS name: ${cleanDnsName}`);
|
||||||
|
|
||||||
// Set up Registry config
|
// Set up Registry config
|
||||||
const config = {
|
const config = getRegistryConfig()
|
||||||
chainId: process.env.REGISTRY_CHAIN_ID!,
|
|
||||||
rpcEndpoint: process.env.REGISTRY_RPC_ENDPOINT!,
|
|
||||||
gqlEndpoint: process.env.REGISTRY_GQL_ENDPOINT!,
|
|
||||||
bondId: process.env.REGISTRY_BOND_ID!,
|
|
||||||
authority: process.env.REGISTRY_AUTHORITY!,
|
|
||||||
privateKey: process.env.REGISTRY_USER_KEY!,
|
|
||||||
fee: {
|
|
||||||
gas: process.env.REGISTRY_GAS || '900000',
|
|
||||||
fees: process.env.REGISTRY_FEES || '900000alnt',
|
|
||||||
gasPrice: '0.001alnt', // Hardcoded valid gas price string with denomination
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Registry config:', {
|
console.log('Registry config:', {
|
||||||
...config,
|
...config,
|
||||||
@ -357,19 +358,10 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const deployerLrn = process.env.DEPLOYER_LRN!;
|
const deployerLrn = process.env.DEPLOYER_LRN!;
|
||||||
|
|
||||||
// Create Registry client instance
|
const registry = getRegistry()
|
||||||
const gasPrice = GasPrice.fromString('0.001alnt');
|
|
||||||
console.log('Using manual gas price:', gasPrice);
|
|
||||||
|
|
||||||
const registry = new Registry(
|
// Create LRN for the application with commit hash
|
||||||
config.gqlEndpoint,
|
const lrn = `lrn://${config.authority}/applications/${appName}-${shortHash}`;
|
||||||
config.rpcEndpoint,
|
|
||||||
{ chainId: config.chainId, gasPrice }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create LRN for the application with commit hash and salt
|
|
||||||
// We already have the salt from earlier, so we use it directly
|
|
||||||
const lrn = `lrn://${config.authority}/applications/${appName}-${shortHash}-${salt}`;
|
|
||||||
|
|
||||||
// Get current timestamp for the meta note
|
// Get current timestamp for the meta note
|
||||||
const timestamp = new Date().toUTCString();
|
const timestamp = new Date().toUTCString();
|
||||||
@ -378,7 +370,7 @@ export async function POST(request: NextRequest) {
|
|||||||
console.log('Step 1: Publishing ApplicationRecord...');
|
console.log('Step 1: Publishing ApplicationRecord...');
|
||||||
const applicationRecord = {
|
const applicationRecord = {
|
||||||
type: 'ApplicationRecord',
|
type: 'ApplicationRecord',
|
||||||
name: `${appName}-${shortHash}-${salt}`, // Include commit hash and salt in the record name
|
name: `${appName}-${shortHash}`, // Include commit hash in the record name
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
app_type: 'webapp',
|
app_type: 'webapp',
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
@ -386,12 +378,6 @@ export async function POST(request: NextRequest) {
|
|||||||
app_version: '0.0.1'
|
app_version: '0.0.1'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create fee for transaction directly
|
|
||||||
const fee = {
|
|
||||||
amount: [{ denom: 'alnt', amount: process.env.REGISTRY_FEES?.replace('alnt', '') || '900000' }],
|
|
||||||
gas: process.env.REGISTRY_GAS || '900000',
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Application record data:', applicationRecord);
|
console.log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// Publish the application record
|
// Publish the application record
|
||||||
@ -405,7 +391,6 @@ export async function POST(request: NextRequest) {
|
|||||||
bondId: config.bondId,
|
bondId: config.bondId,
|
||||||
},
|
},
|
||||||
config.privateKey,
|
config.privateKey,
|
||||||
fee
|
|
||||||
)
|
)
|
||||||
) as { id?: string };
|
) as { id?: string };
|
||||||
|
|
||||||
@ -437,7 +422,6 @@ export async function POST(request: NextRequest) {
|
|||||||
lrn
|
lrn
|
||||||
},
|
},
|
||||||
config.privateKey,
|
config.privateKey,
|
||||||
fee
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.log(`Set name mapping: ${lrn} -> ${applicationRecordId}`);
|
console.log(`Set name mapping: ${lrn} -> ${applicationRecordId}`);
|
||||||
@ -450,7 +434,6 @@ export async function POST(request: NextRequest) {
|
|||||||
lrn: `${lrn}@${fullHash}`
|
lrn: `${lrn}@${fullHash}`
|
||||||
},
|
},
|
||||||
config.privateKey,
|
config.privateKey,
|
||||||
fee
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.log(`Set name mapping: ${lrn}@${fullHash} -> ${applicationRecordId}`);
|
console.log(`Set name mapping: ${lrn}@${fullHash} -> ${applicationRecordId}`);
|
||||||
@ -468,21 +451,25 @@ export async function POST(request: NextRequest) {
|
|||||||
const deploymentRequestData = {
|
const deploymentRequestData = {
|
||||||
type: 'ApplicationDeploymentRequest',
|
type: 'ApplicationDeploymentRequest',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
name: `${appName}-${shortHash}-${salt}`, // Update name to match application record
|
name: `${appName}-${shortHash}`, // Update name to match application record
|
||||||
application: lrn, // LRN already includes commit hash and salt
|
application: lrn, // LRN already includes commit hash
|
||||||
deployer: deployerLrn,
|
deployer: deployerLrn,
|
||||||
dns: cleanDnsName,
|
dns: cleanDnsName,
|
||||||
config: {
|
config: {
|
||||||
env: {
|
// TODO: Add feature to set env config for app
|
||||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: process.env.REGISTRY_CHAIN_ID || 'laconic-testnet-2'
|
env: {}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
note: `Added via ATOM-Deploy @ ${timestamp}`,
|
note: `Added via ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL}-Deploy @ ${timestamp}`,
|
||||||
repository: repoUrl,
|
repository: repoUrl,
|
||||||
repository_ref: fullHash,
|
repository_ref: fullHash,
|
||||||
},
|
},
|
||||||
payment: txHash,
|
external_payment: {
|
||||||
|
chain_id: paymentMethod === PaymentMethod.SPL_TOKEN ? SOLANA_CHAIN_ID : GORBAGANA_CHAIN_ID,
|
||||||
|
tx_hash: txHash,
|
||||||
|
pubkey: senderPublicKey
|
||||||
|
},
|
||||||
|
payment: laconicTxHash,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Deployment request data:', deploymentRequestData);
|
console.log('Deployment request data:', deploymentRequestData);
|
||||||
@ -498,7 +485,6 @@ export async function POST(request: NextRequest) {
|
|||||||
bondId: config.bondId,
|
bondId: config.bondId,
|
||||||
},
|
},
|
||||||
config.privateKey,
|
config.privateKey,
|
||||||
fee
|
|
||||||
)
|
)
|
||||||
) as { id?: string };
|
) as { id?: string };
|
||||||
|
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
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",
|
||||||
@ -14,8 +18,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Deploy Frontends using ATOM and Laconic",
|
title: `Deploy Frontends using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} and Laconic`,
|
||||||
description: "Deploy URLs to Laconic Registry using ATOM payments",
|
description: `Deploy URLs to Laconic Registry using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payments`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -29,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>
|
||||||
);
|
);
|
||||||
|
285
src/app/page.tsx
285
src/app/page.tsx
@ -1,17 +1,44 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||||
// Dynamically import Keplr component to avoid SSR issues with browser APIs
|
|
||||||
const KeplrConnect = dynamic(() => import('@/components/KeplrConnect'), { ssr: false });
|
|
||||||
import URLForm from '@/components/URLForm';
|
|
||||||
// Dynamically import PaymentModal component to avoid SSR issues with browser APIs
|
|
||||||
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
|
||||||
import StatusDisplay from '@/components/StatusDisplay';
|
|
||||||
import { createApplicationDeploymentRequest } from '@/services/registry';
|
|
||||||
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 StatusDisplay from '@/components/StatusDisplay';
|
||||||
|
import { createApplicationDeploymentRequest } from '@/services/registry';
|
||||||
|
import { PAYMENT_METHOD_LABELS } from '@/constants/payments';
|
||||||
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
import { PaymentMethod } from '@/types';
|
||||||
|
import { IS_NAT_GOR_TRANSFER_ENABLED } from '@/utils/gorbagana';
|
||||||
|
|
||||||
|
// Dynamically import components to avoid SSR issues with browser APIs
|
||||||
|
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
|
||||||
|
|
||||||
|
// Use following curl request to get Gorbagana chain genesis hash:
|
||||||
|
// curl https://rpc.gorbagana.wtf \
|
||||||
|
// -X POST \
|
||||||
|
// -H "Content-Type: application/json" \
|
||||||
|
// --data '{"jsonrpc":"2.0","id":1,"method":"getGenesisHash"}'
|
||||||
|
//
|
||||||
|
// RPC endpoint reference: https://docs.gorbagana.wtf/testnet-v2-devnet.html
|
||||||
|
const GORBAGANA_GENESIS_HASH = '533uBE9RRquhTBqEX58oV52FdTTsReMdAvaUvP6hNjsn';
|
||||||
|
|
||||||
|
// Use following curl request to get Solana chain genesis hash:
|
||||||
|
// curl https://api.mainnet-beta.solana.com \
|
||||||
|
// -X POST \
|
||||||
|
// -H "Content-Type: application/json" \
|
||||||
|
// --data '{"jsonrpc":"2.0","id":1,"method":"getGenesisHash"}'
|
||||||
|
// RPC endpoint reference: https://solana.com/docs/references/clusters#on-a-high-level
|
||||||
|
const SOLANA_GENESIS_HASH = '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [walletAddress, setWalletAddress] = useState<string | null>(null);
|
const { wallet, connected, publicKey, disconnect } = useWallet();
|
||||||
|
|
||||||
|
const { selectedPaymentMethod, setSelectedPaymentMethod } = usePaymentMethod();
|
||||||
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');
|
||||||
@ -22,28 +49,99 @@ 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 [incorrectChainWarning, setIncorrectChainWarning] = useState<string | null>(null);
|
||||||
|
const [isFetchingChainGenesisHash, setIsFetchingChainGenesisHash] = useState(false);
|
||||||
|
|
||||||
const handleConnect = (address: string) => {
|
useEffect(() => {
|
||||||
setWalletAddress(address);
|
if (!IS_NAT_GOR_TRANSFER_ENABLED) {
|
||||||
};
|
setSelectedPaymentMethod(PaymentMethod.SPL_TOKEN);
|
||||||
|
}
|
||||||
|
}, [setSelectedPaymentMethod]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIncorrectChainWarning(null);
|
||||||
|
if (!wallet || wallet.adapter.name !== BackpackWalletName || !connected || !selectedPaymentMethod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnOnIncorrectChain = async () => {
|
||||||
|
setIsFetchingChainGenesisHash(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @ts-expect-error: backpack exists on window object
|
||||||
|
const genesisHash = await window.backpack.solana.connection.getGenesisHash();
|
||||||
|
|
||||||
|
const expectedGenesisHash = selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||||
|
? GORBAGANA_GENESIS_HASH
|
||||||
|
: SOLANA_GENESIS_HASH;
|
||||||
|
|
||||||
|
const expectedChainName = selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||||
|
? "Gorbagana"
|
||||||
|
: "Solana";
|
||||||
|
|
||||||
|
if (genesisHash !== expectedGenesisHash) {
|
||||||
|
setIncorrectChainWarning(
|
||||||
|
`Unsupported network selected in wallet. Please switch to network for ${expectedChainName} chain and reconnect the wallet.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsFetchingChainGenesisHash(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warnOnIncorrectChain();
|
||||||
|
}, [wallet, selectedPaymentMethod, connected]);
|
||||||
|
|
||||||
|
// 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 = async (hash: string) => {
|
// Helper function to check if current wallet is compatible with selected payment method
|
||||||
|
const isWalletCompatible = useMemo(() => {
|
||||||
|
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 true; // Only non-Backpack wallets for SPL tokens
|
||||||
|
}
|
||||||
|
}, [selectedPaymentMethod, wallet]);
|
||||||
|
|
||||||
|
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
|
||||||
|
if (!publicKey || !url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setTxHash(hash);
|
setTxHash(hash);
|
||||||
setShowPaymentModal(false);
|
setShowPaymentModal(false);
|
||||||
setStatus('creating');
|
setStatus('creating');
|
||||||
|
|
||||||
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)
|
||||||
if (url) {
|
const result = await createApplicationDeploymentRequest(url, hash, paymentMethod);
|
||||||
const result = await createApplicationDeploymentRequest(url, hash);
|
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
setRecordId(result.id);
|
setRecordId(result.id);
|
||||||
@ -62,23 +160,16 @@ 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');
|
||||||
setError(result.message || 'Failed to create record in Laconic Registry');
|
setError(result.message || 'Failed to create record in Laconic Registry');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
||||||
}
|
}
|
||||||
};
|
}, [publicKey, url])
|
||||||
|
|
||||||
const handleClosePaymentModal = () => {
|
const handleClosePaymentModal = () => {
|
||||||
setShowPaymentModal(false);
|
setShowPaymentModal(false);
|
||||||
@ -89,35 +180,153 @@ export default function Home() {
|
|||||||
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
|
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
|
||||||
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
||||||
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
|
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
|
||||||
Deploy Frontends with ATOM and 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>
|
||||||
<KeplrConnect onConnect={handleConnect} />
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedPaymentMethod(PaymentMethod.NAT_GOR)}
|
||||||
|
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={{
|
||||||
|
backgroundColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--accent)' : 'var(--card-bg)',
|
||||||
|
borderColor: selectedPaymentMethod === PaymentMethod.NAT_GOR ? 'var(--primary)' : 'var(--card-border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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: All Solana compatible wallets
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
{ isFetchingChainGenesisHash && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Checking wallet network compatibility...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{incorrectChainWarning && (
|
||||||
|
<p className="text-sm text-amber-400">
|
||||||
|
{incorrectChainWarning}
|
||||||
|
</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>
|
||||||
|
|
||||||
<div className="mb-8 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)', opacity: walletAddress ? '1' : '0.6' }}>
|
{/* Step 3: URL Input */}
|
||||||
|
<div className="mb-8 p-6 rounded-lg" style={{
|
||||||
|
background: 'var(--muted-light)',
|
||||||
|
borderLeft: '4px solid var(--primary)',
|
||||||
|
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={!walletAddress || status === 'creating'}
|
disabled={!connected || !isWalletCompatible || isFetchingChainGenesisHash || status === 'creating' || Boolean(incorrectChainWarning)}
|
||||||
/>
|
/>
|
||||||
</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
|
||||||
@ -129,15 +338,13 @@ 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 && walletAddress && url && (
|
{showPaymentModal && url && connected && publicKey && selectedPaymentMethod && (
|
||||||
<PaymentModal
|
<PaymentModal
|
||||||
isOpen={showPaymentModal}
|
isOpen={showPaymentModal}
|
||||||
onClose={handleClosePaymentModal}
|
onClose={handleClosePaymentModal}
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { connectKeplr } from '@/services/keplr';
|
|
||||||
|
|
||||||
interface KeplrConnectProps {
|
|
||||||
onConnect: (address: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function KeplrConnect({ onConnect }: KeplrConnectProps) {
|
|
||||||
const [connecting, setConnecting] = useState(false);
|
|
||||||
const [address, setAddress] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleConnect = async () => {
|
|
||||||
setConnecting(true);
|
|
||||||
try {
|
|
||||||
const userAddress = await connectKeplr();
|
|
||||||
if (userAddress) {
|
|
||||||
setAddress(userAddress);
|
|
||||||
onConnect(userAddress);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to Keplr:', error);
|
|
||||||
} finally {
|
|
||||||
setConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check if Keplr is available
|
|
||||||
if (typeof window !== 'undefined' && window.keplr) {
|
|
||||||
// Auto-connect on page load
|
|
||||||
handleConnect();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center p-4 rounded-lg">
|
|
||||||
{address ? (
|
|
||||||
<div className="flex flex-col items-center w-full">
|
|
||||||
<div className="flex items-center mb-2">
|
|
||||||
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
|
|
||||||
<p className="font-medium" style={{ color: 'var(--success)' }}>Connected</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
|
||||||
<p className="text-sm font-mono break-all text-center">{address}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleConnect}
|
|
||||||
disabled={connecting}
|
|
||||||
className="px-6 py-3 rounded-md w-full sm:w-auto transition-colors"
|
|
||||||
style={{
|
|
||||||
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
|
|
||||||
color: 'var(--primary-foreground)',
|
|
||||||
opacity: connecting ? '0.8' : '1',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{connecting && (
|
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
{connecting ? 'Connecting...' : 'Connect Keplr Wallet'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,14 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { sendAtomPayment } from '@/services/keplr';
|
import assert from 'assert';
|
||||||
|
|
||||||
interface PaymentModalProps {
|
import { Connection } from '@solana/web3.js';
|
||||||
isOpen: boolean;
|
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
|
||||||
onClose: () => void;
|
|
||||||
url: string;
|
import { sendSolanaPayment } from '@/services/solana';
|
||||||
onPaymentComplete: (txHash: string) => void;
|
import { getRequiredTokenInfo, RequiredTokenInfo } from '@/services/jupiter-price';
|
||||||
}
|
import { PaymentMethod, PaymentModalProps, PaymentRequest } from '@/types';
|
||||||
|
import { PAYMENT_METHOD_LABELS, SOLANA_GOR_MINT_ADDRESS } from '@/constants/payments';
|
||||||
|
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
|
||||||
|
import { getCostOfDeployment } from '@/services/registry';
|
||||||
|
import { IS_NAT_GOR_TRANSFER_ENABLED } from '@/utils/gorbagana';
|
||||||
|
|
||||||
|
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 GORBAGANA_RPC_URL = process.env.NEXT_PUBLIC_GORBAGANA_RPC_URL;
|
||||||
|
|
||||||
export default function PaymentModal({
|
export default function PaymentModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -16,29 +24,84 @@ export default function PaymentModal({
|
|||||||
url,
|
url,
|
||||||
onPaymentComplete,
|
onPaymentComplete,
|
||||||
}: PaymentModalProps) {
|
}: PaymentModalProps) {
|
||||||
const [amount, setAmount] = useState('0.01');
|
const { selectedPaymentMethod: paymentMethod } = usePaymentMethod();
|
||||||
|
const { connection: solanaConnection } = useConnection();
|
||||||
|
const { wallet, publicKey } = useWallet();
|
||||||
|
|
||||||
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 [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback
|
||||||
|
const [loadingPrice, setLoadingPrice] = useState(true);
|
||||||
|
const [deploymentCost, setDeploymentCost] = useState<number | null>(null);
|
||||||
|
|
||||||
// Get recipient address from environment variables
|
useEffect(() => {
|
||||||
const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
|
const getDeploymentCostInfo = async () => {
|
||||||
|
const cost = await getCostOfDeployment();
|
||||||
|
|
||||||
// Validate amount on change
|
setDeploymentCost(cost);
|
||||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
}
|
||||||
const value = e.target.value;
|
|
||||||
setAmount(value);
|
|
||||||
|
|
||||||
// Clear error when user types
|
getDeploymentCostInfo();
|
||||||
if (error) {
|
}, []);
|
||||||
|
|
||||||
|
const mintAddress = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS!;
|
||||||
|
const tokenSymbol = process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL;
|
||||||
|
|
||||||
|
// Fetch payment amount based on USD price for both payment methods
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !deploymentCost || !paymentMethod) return;
|
||||||
|
|
||||||
|
const fetchPaymentAmount = async () => {
|
||||||
|
setLoadingPrice(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let requiredTokenInfo: RequiredTokenInfo
|
||||||
|
if (paymentMethod === PaymentMethod.NAT_GOR) {
|
||||||
|
// Fetch native GOR amount using solana GOR token price
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(deploymentCost, SOLANA_GOR_MINT_ADDRESS);
|
||||||
|
} else if (paymentMethod === PaymentMethod.SPL_TOKEN) {
|
||||||
|
// Fetch SPL token amount using token mint price
|
||||||
|
requiredTokenInfo = await getRequiredTokenInfo(deploymentCost, mintAddress);
|
||||||
|
} else {
|
||||||
|
setError('Invalid payment method');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTokenAmount(requiredTokenInfo.requiredAmountInBaseUnits);
|
||||||
|
setTokenDecimals(requiredTokenInfo.decimals);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching payment amount:', error);
|
||||||
|
setError('Unable to fetch current payment amount. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoadingPrice(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePayment = async () => {
|
fetchPaymentAmount();
|
||||||
// Validate amount before sending
|
}, [isOpen, paymentMethod, deploymentCost, mintAddress]);
|
||||||
const parsedAmount = parseFloat(amount);
|
|
||||||
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
// Initialize state when modal opens
|
||||||
setError('Please enter a valid positive amount');
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setError('');
|
||||||
|
setTokenAmount(0);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handlePayment = useCallback(async () => {
|
||||||
|
if (!paymentMethod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenAmount === 0 || loadingPrice) {
|
||||||
|
setError('Payment amount not ready. Please wait.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wallet || !wallet.adapter) {
|
||||||
|
setError('Wallet not connected.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,28 +109,51 @@ export default function PaymentModal({
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sendAtomPayment(recipientAddress, amount);
|
const paymentRequest: PaymentRequest = {
|
||||||
|
paymentMethod: paymentMethod,
|
||||||
|
amount: tokenAmount,
|
||||||
|
};
|
||||||
|
|
||||||
if (result.status === 'success' && result.hash) {
|
// Use different RPC connection based on payment method
|
||||||
onPaymentComplete(result.hash);
|
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) {
|
||||||
|
onPaymentComplete(result.transactionSignature, paymentMethod);
|
||||||
} else {
|
} else {
|
||||||
setError(result.message || '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);
|
||||||
}
|
}
|
||||||
|
}, [paymentMethod, tokenAmount, loadingPrice, wallet, solanaConnection, publicKey, onPaymentComplete]);
|
||||||
|
|
||||||
|
const getPaymentAmountDisplay = () => {
|
||||||
|
if (loadingPrice) {
|
||||||
|
return 'Loading...'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
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)' }}>
|
||||||
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
|
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
|
||||||
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)' }}>Complete Payment</h2>
|
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
|
||||||
|
Complete Payment
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@ -78,36 +164,105 @@ 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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="amount" className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
|
||||||
Amount (ATOM)
|
Payment Amount
|
||||||
</label>
|
</label>
|
||||||
|
<div className="space-y-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
{loadingPrice ? (
|
||||||
|
<div className="w-full p-3 rounded-md flex items-center" style={{
|
||||||
|
background: 'var(--muted-light)',
|
||||||
|
border: '1px solid var(--input-border)',
|
||||||
|
color: 'var(--muted)'
|
||||||
|
}}>
|
||||||
|
<svg className="animate-spin h-4 w-4 mr-2" 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>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<input
|
<input
|
||||||
id="amount"
|
type="text"
|
||||||
type="number"
|
value={`$${deploymentCost ? deploymentCost.toPrecision(2) : null}`}
|
||||||
min="0.01"
|
disabled={true}
|
||||||
step="0.01"
|
|
||||||
value={amount}
|
|
||||||
onChange={handleAmountChange}
|
|
||||||
className="w-full p-3 pr-12 rounded-md"
|
className="w-full p-3 pr-12 rounded-md"
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--card-bg)',
|
background: 'var(--card-bg)',
|
||||||
border: '1px solid var(--input-border)',
|
border: '1px solid var(--input-border)',
|
||||||
color: 'var(--foreground)'
|
color: 'var(--foreground)',
|
||||||
|
opacity: '0.7'
|
||||||
}}
|
}}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<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)' }}>ATOM</span>
|
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>USD</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{loadingPrice ? (
|
||||||
|
<div className="w-full p-3 rounded-md flex items-center" style={{
|
||||||
|
background: 'var(--muted-light)',
|
||||||
|
border: '1px solid var(--input-border)',
|
||||||
|
color: 'var(--muted)'
|
||||||
|
}}>
|
||||||
|
<svg className="animate-spin h-4 w-4 mr-2" 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>
|
||||||
|
Fetching token amount...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={getPaymentAmountDisplay()}
|
||||||
|
disabled={true}
|
||||||
|
className="w-full p-3 pr-20 rounded-md"
|
||||||
|
style={{
|
||||||
|
background: 'var(--card-bg)',
|
||||||
|
border: '1px solid var(--input-border)',
|
||||||
|
color: 'var(--foreground)',
|
||||||
|
opacity: '0.7'
|
||||||
|
}}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!(paymentMethod === PaymentMethod.SPL_TOKEN && loadingPrice) && (
|
||||||
|
<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)' }}>
|
||||||
|
{paymentMethod === PaymentMethod.NAT_GOR ? 'GOR (native)' : tokenSymbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{paymentMethod === PaymentMethod.SPL_TOKEN && (
|
||||||
|
<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/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@ -134,19 +289,21 @@ export default function PaymentModal({
|
|||||||
onClick={handlePayment}
|
onClick={handlePayment}
|
||||||
className="px-5 py-2 rounded-md flex items-center transition-colors"
|
className="px-5 py-2 rounded-md flex items-center transition-colors"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: loading ? 'var(--muted)' : 'var(--primary)',
|
backgroundColor: (loading || loadingPrice) ? 'var(--muted)' : 'var(--primary)',
|
||||||
color: 'var(--primary-foreground)',
|
color: 'var(--primary-foreground)',
|
||||||
opacity: loading ? '0.8' : '1'
|
opacity: (loading || loadingPrice) ? '0.8' : '1'
|
||||||
}}
|
}}
|
||||||
disabled={loading}
|
disabled={loading || loadingPrice}
|
||||||
>
|
>
|
||||||
{loading && (
|
{(loading || loadingPrice) && (
|
||||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" 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>
|
<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>
|
<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>
|
||||||
)}
|
)}
|
||||||
{loading ? 'Processing...' : 'Pay with Keplr'}
|
{loadingPrice ? 'Loading Price...' :
|
||||||
|
loading ? 'Processing...' :
|
||||||
|
'Pay with Solana Wallet'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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,12 +21,10 @@ 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
|
||||||
const domainSuffix = process.env.NEXT_PUBLIC_DOMAIN_SUFFIX || '';
|
const domainSuffix = process.env.NEXT_PUBLIC_DOMAIN_SUFFIX;
|
||||||
if (status === 'idle') return null;
|
if (status === 'idle') return null;
|
||||||
|
|
||||||
const StatusBadge = ({ type }: { type: 'creating' | 'success' | 'error' }) => {
|
const StatusBadge = ({ type }: { type: 'creating' | 'success' | 'error' }) => {
|
||||||
@ -125,11 +121,15 @@ export default function StatusDisplay({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{txHash && <InfoItem label="ATOM Payment Transaction Hash" value={txHash} />}
|
{txHash && <InfoItem label={`${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Payment Transaction Hash`} value={txHash} />}
|
||||||
{appRecordId && <InfoItem label="Laconic Application Record ID" value={appRecordId} />}
|
{appRecordId && <InfoItem label="Laconic Application Record ID" value={appRecordId} />}
|
||||||
{recordId && <InfoItem label="Laconic Deployment Request Record ID" value={recordId} />}
|
{recordId && <InfoItem label="Laconic Deployment Request Record ID" value={recordId} />}
|
||||||
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
||||||
{dns && <InfoItem label="Deployment URL (ready in 2-3 minutes)" value={domainSuffix ? `https://${dns}${domainSuffix}` : `http://${dns}`} />}
|
{dns && (
|
||||||
|
domainSuffix
|
||||||
|
? <InfoItem label="Deployment URL (ready in 2-3 minutes)" value={`https://${dns}.${domainSuffix}`} />
|
||||||
|
: <InfoItem label="Please set domain suffix" value="" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ interface URLFormProps {
|
|||||||
export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||||
// Get example URL from environment variables or use a default
|
// Get example URL from environment variables or use a default
|
||||||
const exampleUrl = process.env.NEXT_PUBLIC_EXAMPLE_URL || 'https://example.com';
|
const exampleUrl = process.env.NEXT_PUBLIC_EXAMPLE_URL || 'https://example.com';
|
||||||
const [url, setUrl] = useState(exampleUrl);
|
const [url, setUrl] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
@ -49,7 +49,8 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
|||||||
// All validations passed
|
// All validations passed
|
||||||
setError('');
|
setError('');
|
||||||
onSubmit(trimmedUrl);
|
onSubmit(trimmedUrl);
|
||||||
} catch (_) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
setError('Please enter a valid URL (e.g., https://example.com)');
|
setError('Please enter a valid URL (e.g., https://example.com)');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -102,7 +103,7 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
|||||||
opacity: (disabled || !url) ? '0.7' : '1',
|
opacity: (disabled || !url) ? '0.7' : '1',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{disabled ? 'Connect Wallet First' : 'Deploy URL'}
|
Deploy URL
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,43 @@
|
|||||||
|
import { DENOM as ALNT_DENOM, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
import { RegistryConfig } from '../types';
|
import { RegistryConfig } from '../types';
|
||||||
|
import { GasPrice } from '@cosmjs/stargate';
|
||||||
|
|
||||||
|
let registryInstance: Registry | null = null;
|
||||||
|
|
||||||
|
|
||||||
|
export const getRegistry = (): Registry => {
|
||||||
|
if (!registryInstance) {
|
||||||
|
const config = getClientRegistryConfig();
|
||||||
|
const REGISTRY_GAS_PRICE = process.env.REGISTRY_GAS_PRICE;
|
||||||
|
const gasPrice = REGISTRY_GAS_PRICE ? GasPrice.fromString( REGISTRY_GAS_PRICE + ALNT_DENOM) : undefined;
|
||||||
|
|
||||||
|
registryInstance = new Registry(
|
||||||
|
config.gqlEndpoint,
|
||||||
|
config.rpcEndpoint,
|
||||||
|
{
|
||||||
|
chainId: config.chainId,
|
||||||
|
gasPrice,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return registryInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getClientRegistryConfig = () => {
|
||||||
|
return {
|
||||||
|
chainId: process.env.NEXT_PUBLIC_REGISTRY_CHAIN_ID!,
|
||||||
|
rpcEndpoint: process.env.NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT!,
|
||||||
|
gqlEndpoint: process.env.NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT!,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const getRegistryConfig = (): RegistryConfig => {
|
export const getRegistryConfig = (): RegistryConfig => {
|
||||||
// Validate required environment variables
|
// Validate required environment variables
|
||||||
const requiredEnvVars = [
|
const requiredEnvVars = [
|
||||||
'REGISTRY_CHAIN_ID',
|
'NEXT_PUBLIC_REGISTRY_CHAIN_ID',
|
||||||
'REGISTRY_GQL_ENDPOINT',
|
'NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT',
|
||||||
'REGISTRY_RPC_ENDPOINT',
|
'NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT',
|
||||||
'REGISTRY_BOND_ID',
|
'REGISTRY_BOND_ID',
|
||||||
'REGISTRY_AUTHORITY',
|
'REGISTRY_AUTHORITY',
|
||||||
'REGISTRY_USER_KEY'
|
'REGISTRY_USER_KEY'
|
||||||
@ -18,29 +50,11 @@ export const getRegistryConfig = (): RegistryConfig => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chainId: process.env.REGISTRY_CHAIN_ID!,
|
chainId: process.env.NEXT_PUBLIC_REGISTRY_CHAIN_ID!,
|
||||||
rpcEndpoint: process.env.REGISTRY_RPC_ENDPOINT!,
|
rpcEndpoint: process.env.NEXT_PUBLIC_REGISTRY_RPC_ENDPOINT!,
|
||||||
gqlEndpoint: process.env.REGISTRY_GQL_ENDPOINT!,
|
gqlEndpoint: process.env.NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT!,
|
||||||
bondId: process.env.REGISTRY_BOND_ID!,
|
bondId: process.env.REGISTRY_BOND_ID!,
|
||||||
authority: process.env.REGISTRY_AUTHORITY!,
|
authority: process.env.REGISTRY_AUTHORITY!,
|
||||||
privateKey: process.env.REGISTRY_USER_KEY!,
|
privateKey: process.env.REGISTRY_USER_KEY!,
|
||||||
fee: {
|
|
||||||
gas: process.env.REGISTRY_GAS || '900000',
|
|
||||||
fees: process.env.REGISTRY_FEES || '900000alnt',
|
|
||||||
gasPrice: process.env.REGISTRY_GAS_PRICE || '0.025',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDeployerLrn = (): string => {
|
|
||||||
if (!process.env.DEPLOYER_LRN) {
|
|
||||||
throw new Error('Missing environment variable: DEPLOYER_LRN');
|
|
||||||
}
|
|
||||||
return process.env.DEPLOYER_LRN;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAppName = (): string => {
|
|
||||||
return process.env.APP_NAME || 'atom-deploy';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const COSMOS_DENOM = 'uatom';
|
|
9
src/constants/payments.ts
Normal file
9
src/constants/payments.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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';
|
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;
|
||||||
|
}
|
67
src/services/jupiter-price.ts
Normal file
67
src/services/jupiter-price.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
interface JupiterPriceResponse {
|
||||||
|
[mintAddress: string]: {
|
||||||
|
usdPrice: number;
|
||||||
|
blockId: number;
|
||||||
|
decimals: number;
|
||||||
|
priceChange24h: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenPriceInfo {
|
||||||
|
usdPrice: number;
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequiredTokenInfo {
|
||||||
|
requiredAmountInBaseUnits: number;
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches token price from Jupiter aggregator API
|
||||||
|
* @param mintAddress The Solana token mint address
|
||||||
|
* @returns Token price information including USD price and decimals
|
||||||
|
*/
|
||||||
|
async function getTokenInfo(mintAddress: string): Promise<TokenPriceInfo> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://lite-api.jup.ag/price/v3?ids=${mintAddress}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Jupiter API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: JupiterPriceResponse = await response.json();
|
||||||
|
|
||||||
|
if (!data[mintAddress]) {
|
||||||
|
throw new Error(`Price not found for token: ${mintAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenInfo = data[mintAddress];
|
||||||
|
|
||||||
|
return {
|
||||||
|
usdPrice: tokenInfo.usdPrice,
|
||||||
|
decimals: tokenInfo.decimals
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching token price from Jupiter:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the token amount needed for a given USD price
|
||||||
|
* @param targetUsdAmount The target amount in USD
|
||||||
|
* @param mintAddress The Solana token mint address
|
||||||
|
* @returns The token amount (in smallest units) needed and token decimals
|
||||||
|
*/
|
||||||
|
export async function getRequiredTokenInfo(targetUsdAmount: number, mintAddress: string): Promise<RequiredTokenInfo> {
|
||||||
|
const priceInfo = await getTokenInfo(mintAddress);
|
||||||
|
|
||||||
|
// Calculate token amount needed
|
||||||
|
const tokenAmount = targetUsdAmount / priceInfo.usdPrice;
|
||||||
|
|
||||||
|
// Convert to smallest units (considering decimals)
|
||||||
|
const requiredAmountInBaseUnits = Math.round(tokenAmount * Math.pow(10, priceInfo.decimals));
|
||||||
|
|
||||||
|
return {requiredAmountInBaseUnits, decimals: priceInfo.decimals};
|
||||||
|
}
|
@ -1,260 +0,0 @@
|
|||||||
import { SigningStargateClient } from '@cosmjs/stargate';
|
|
||||||
import { TransactionResponse } from '../types';
|
|
||||||
import { COSMOS_DENOM } from '../config';
|
|
||||||
|
|
||||||
export const connectKeplr = async (): Promise<string | null> => {
|
|
||||||
if (!window.keplr) {
|
|
||||||
alert('Keplr wallet extension is not installed!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const chainId = process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4';
|
|
||||||
|
|
||||||
// Try to suggest chain if custom network
|
|
||||||
if (chainId !== 'cosmoshub-4') {
|
|
||||||
try {
|
|
||||||
// Check if we need to suggest the chain to Keplr
|
|
||||||
await window.keplr.getKey(chainId).catch(async () => {
|
|
||||||
// Chain needs to be suggested
|
|
||||||
if (process.env.NEXT_PUBLIC_COSMOS_RPC_URL) {
|
|
||||||
await window.keplr.experimentalSuggestChain({
|
|
||||||
chainId: chainId,
|
|
||||||
chainName: chainId,
|
|
||||||
rpc: process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
|
||||||
rest: process.env.NEXT_PUBLIC_COSMOS_REST_URL || process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
|
||||||
bip44: {
|
|
||||||
coinType: 118,
|
|
||||||
},
|
|
||||||
bech32Config: {
|
|
||||||
bech32PrefixAccAddr: "cosmos",
|
|
||||||
bech32PrefixAccPub: "cosmospub",
|
|
||||||
bech32PrefixValAddr: "cosmosvaloper",
|
|
||||||
bech32PrefixValPub: "cosmosvaloperpub",
|
|
||||||
bech32PrefixConsAddr: "cosmosvalcons",
|
|
||||||
bech32PrefixConsPub: "cosmosvalconspub",
|
|
||||||
},
|
|
||||||
currencies: [
|
|
||||||
{
|
|
||||||
coinDenom: "ATOM",
|
|
||||||
coinMinimalDenom: "uatom",
|
|
||||||
coinDecimals: 6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
feeCurrencies: [
|
|
||||||
{
|
|
||||||
coinDenom: "ATOM",
|
|
||||||
coinMinimalDenom: "uatom",
|
|
||||||
coinDecimals: 6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stakeCurrency: {
|
|
||||||
coinDenom: "ATOM",
|
|
||||||
coinMinimalDenom: "uatom",
|
|
||||||
coinDecimals: 6,
|
|
||||||
},
|
|
||||||
gasPriceStep: {
|
|
||||||
low: 0.01,
|
|
||||||
average: 0.025,
|
|
||||||
high: 0.04,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (suggestError) {
|
|
||||||
console.warn("Failed to suggest chain to Keplr:", suggestError);
|
|
||||||
// Continue anyway, as enable might still work
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable Keplr for the specified chain
|
|
||||||
await window.keplr.enable(chainId);
|
|
||||||
const offlineSigner = window.keplr.getOfflineSigner(chainId);
|
|
||||||
|
|
||||||
// Get the user's account
|
|
||||||
const accounts = await offlineSigner.getAccounts();
|
|
||||||
if (!accounts || accounts.length === 0) {
|
|
||||||
console.error('No accounts found in Keplr wallet');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return accounts[0].address;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to Keplr wallet:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendAtomPayment = async (
|
|
||||||
recipientAddress: string,
|
|
||||||
amount: string
|
|
||||||
): Promise<TransactionResponse> => {
|
|
||||||
try {
|
|
||||||
if (!window.keplr) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Keplr wallet extension is not installed!'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate recipient address is a valid cosmos address
|
|
||||||
if (!recipientAddress || !recipientAddress.startsWith('cosmos1')) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Invalid recipient address. Must be a valid Cosmos address starting with cosmos1'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate amount is a positive number
|
|
||||||
const parsedAmount = parseFloat(amount);
|
|
||||||
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Invalid amount. Must be a positive number'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the chain ID from environment variables or use default
|
|
||||||
const chainId = process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4';
|
|
||||||
|
|
||||||
// Enable the chain in Keplr, following same logic as connectKeplr
|
|
||||||
if (chainId !== 'cosmoshub-4') {
|
|
||||||
try {
|
|
||||||
// Check if we need to suggest the chain to Keplr
|
|
||||||
await window.keplr.getKey(chainId).catch(async () => {
|
|
||||||
// Chain needs to be suggested
|
|
||||||
if (process.env.NEXT_PUBLIC_COSMOS_RPC_URL) {
|
|
||||||
await window.keplr.experimentalSuggestChain({
|
|
||||||
chainId: chainId,
|
|
||||||
chainName: chainId,
|
|
||||||
rpc: process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
|
||||||
rest: process.env.NEXT_PUBLIC_COSMOS_REST_URL || process.env.NEXT_PUBLIC_COSMOS_RPC_URL,
|
|
||||||
bip44: { coinType: 118 },
|
|
||||||
bech32Config: {
|
|
||||||
bech32PrefixAccAddr: "cosmos",
|
|
||||||
bech32PrefixAccPub: "cosmospub",
|
|
||||||
bech32PrefixValAddr: "cosmosvaloper",
|
|
||||||
bech32PrefixValPub: "cosmosvaloperpub",
|
|
||||||
bech32PrefixConsAddr: "cosmosvalcons",
|
|
||||||
bech32PrefixConsPub: "cosmosvalconspub",
|
|
||||||
},
|
|
||||||
currencies: [{ coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 }],
|
|
||||||
feeCurrencies: [{ coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 }],
|
|
||||||
stakeCurrency: { coinDenom: "ATOM", coinMinimalDenom: "uatom", coinDecimals: 6 },
|
|
||||||
gasPriceStep: { low: 0.01, average: 0.025, high: 0.04 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (suggestError) {
|
|
||||||
console.warn("Failed to suggest chain to Keplr:", suggestError);
|
|
||||||
// Continue anyway, as enable might still work
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable the chain in Keplr
|
|
||||||
await window.keplr.enable(chainId);
|
|
||||||
const offlineSigner = window.keplr.getOfflineSigner(chainId);
|
|
||||||
|
|
||||||
// Create the Stargate client
|
|
||||||
const rpcEndpoint = process.env.NEXT_PUBLIC_COSMOS_RPC_URL;
|
|
||||||
if (!rpcEndpoint) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'NEXT_PUBLIC_COSMOS_RPC_URL environment variable is not set'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await SigningStargateClient.connectWithSigner(
|
|
||||||
rpcEndpoint,
|
|
||||||
offlineSigner
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the user's account
|
|
||||||
const accounts = await offlineSigner.getAccounts();
|
|
||||||
if (!accounts || accounts.length === 0) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'No accounts found in Keplr wallet'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const sender = accounts[0].address;
|
|
||||||
|
|
||||||
// Convert amount to microdenom (e.g., ATOM to uatom)
|
|
||||||
const microAmount = convertToMicroDenom(amount);
|
|
||||||
|
|
||||||
// Send the transaction
|
|
||||||
const result = await client.sendTokens(
|
|
||||||
sender,
|
|
||||||
recipientAddress,
|
|
||||||
[{ denom: COSMOS_DENOM, amount: microAmount }],
|
|
||||||
{
|
|
||||||
amount: [{ denom: COSMOS_DENOM, amount: '5000' }],
|
|
||||||
gas: '200000',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result || !result.transactionHash) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Transaction did not return a valid hash'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hash: result.transactionHash,
|
|
||||||
status: 'success',
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to send ATOM payment:', error);
|
|
||||||
// Provide more descriptive error messages for common errors
|
|
||||||
if (error instanceof Error) {
|
|
||||||
const errorMessage = error.message.toLowerCase();
|
|
||||||
|
|
||||||
if (errorMessage.includes('insufficient funds')) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Insufficient funds in your Keplr wallet to complete this transaction'
|
|
||||||
};
|
|
||||||
} else if (errorMessage.includes('rejected')) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Transaction was rejected in the Keplr wallet'
|
|
||||||
};
|
|
||||||
} else if (errorMessage.includes('timeout')) {
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Transaction timed out. Please try again'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hash: '',
|
|
||||||
status: 'error',
|
|
||||||
message: 'Unknown error occurred while sending payment'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to convert from ATOM to uatom (1 ATOM = 1,000,000 uatom)
|
|
||||||
export const convertToMicroDenom = (amount: string): string => {
|
|
||||||
const parsedAmount = parseFloat(amount);
|
|
||||||
if (isNaN(parsedAmount)) {
|
|
||||||
throw new Error('Invalid amount');
|
|
||||||
}
|
|
||||||
return Math.floor(parsedAmount * 1_000_000).toString();
|
|
||||||
};
|
|
115
src/services/laconic-transfer.ts
Normal file
115
src/services/laconic-transfer.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { Account, DENOM as ALNT_DENOM } from '@cerc-io/registry-sdk';
|
||||||
|
import { DeliverTxResponse } from '@cosmjs/stargate';
|
||||||
|
|
||||||
|
import { registryTransactionWithRetry } from '@/app/api/registry/route';
|
||||||
|
import { getRegistry, getRegistryConfig } from '../config';
|
||||||
|
import { LaconicTransferResult } from '../types';
|
||||||
|
|
||||||
|
export const transferLNTTokens = async (): Promise<LaconicTransferResult> => {
|
||||||
|
try {
|
||||||
|
const registryConfig = getRegistryConfig();
|
||||||
|
const registry = getRegistry();
|
||||||
|
|
||||||
|
console.log('Resolving deployer LRN to get payment address...');
|
||||||
|
|
||||||
|
// Resolve the deployer LRN to get the payment address
|
||||||
|
const deployerLrn = process.env.DEPLOYER_LRN;
|
||||||
|
if (!deployerLrn) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'DEPLOYER_LRN environment variable is required'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveResult = await registry.resolveNames([deployerLrn]);
|
||||||
|
console.log('Resolve result:', resolveResult);
|
||||||
|
|
||||||
|
if (!resolveResult || resolveResult.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to resolve deployer LRN: ${deployerLrn}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployerRecord = resolveResult[0];
|
||||||
|
if (!deployerRecord.attributes) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Deployer record has no attributes'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the paymentAddress attribute
|
||||||
|
const paymentAddress = deployerRecord.attributes.paymentAddress
|
||||||
|
const deployerMinPayment = (deployerRecord.attributes.minimumPayment as string).split(ALNT_DENOM)[0]
|
||||||
|
|
||||||
|
console.log('Found payment address:', paymentAddress);
|
||||||
|
console.log('Found minimum payment:', deployerMinPayment);
|
||||||
|
|
||||||
|
console.log('Initiating LNT transfer...');
|
||||||
|
|
||||||
|
// Send tokens from prefilled account to payment address
|
||||||
|
const transferResult = await sendTokensToAccount(
|
||||||
|
registryConfig.privateKey,
|
||||||
|
paymentAddress,
|
||||||
|
deployerMinPayment
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('LNT transfer result:', transferResult);
|
||||||
|
|
||||||
|
if (!transferResult.transactionHash) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'LNT transfer failed - no transaction hash returned'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionHash: transferResult.transactionHash
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to transfer LNT tokens:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error during LNT transfer'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccount = async (accountPrivateKey: string): Promise<Account> => {
|
||||||
|
const account = new Account(
|
||||||
|
Buffer.from(accountPrivateKey, 'hex'),
|
||||||
|
);
|
||||||
|
await account.init();
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendTokensToAccount = async (
|
||||||
|
senderPrivateKey: string,
|
||||||
|
receiverAddress: string,
|
||||||
|
amount: string,
|
||||||
|
): Promise<DeliverTxResponse> => {
|
||||||
|
const registry = getRegistry();
|
||||||
|
|
||||||
|
const account = await getAccount(senderPrivateKey);
|
||||||
|
const laconicClient = await registry.getLaconicClient(account);
|
||||||
|
|
||||||
|
const txResponse: DeliverTxResponse = await registryTransactionWithRetry(
|
||||||
|
() =>
|
||||||
|
laconicClient.sendTokens(
|
||||||
|
account.address,
|
||||||
|
receiverAddress,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
denom: ALNT_DENOM,
|
||||||
|
amount,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"auto",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return txResponse;
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
import axios from 'axios';
|
import assert from 'assert';
|
||||||
import { CreateRecordResponse } from '../types';
|
|
||||||
|
import { getRegistry } from '@/config';
|
||||||
|
import { CreateRecordResponse, PricingRecordAttributes, PaymentMethod } from '../types';
|
||||||
|
|
||||||
|
assert(process.env.NEXT_PUBLIC_DEPLOYMENT_COST_LRN, 'DEPLOYMENT_COST_LRN is required');
|
||||||
|
assert(process.env.NEXT_PUBLIC_ALNT_COST_LRN, 'ALNT_COST_LRN is required');
|
||||||
|
|
||||||
|
const DEPLOYMENT_COST_LRN = process.env.NEXT_PUBLIC_DEPLOYMENT_COST_LRN;
|
||||||
|
const ALNT_COST_LRN = process.env.NEXT_PUBLIC_ALNT_COST_LRN;
|
||||||
|
|
||||||
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}`);
|
console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} payment`);
|
||||||
|
|
||||||
// Call our serverless API endpoint to handle the registry interaction
|
// Call our serverless API endpoint to handle the registry interaction
|
||||||
const response = await fetch('/api/registry', {
|
const response = await fetch('/api/registry', {
|
||||||
@ -14,7 +23,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();
|
||||||
@ -50,63 +59,33 @@ export const createApplicationDeploymentRequest = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyTransaction = async (txHash: string): Promise<boolean> => {
|
const resolvePricingRecordLrns = async (lrns: string[]): Promise<PricingRecordAttributes[]> => {
|
||||||
try {
|
const registry = getRegistry();
|
||||||
// Use the public Cosmos API URL for verification queries
|
const result = await registry.resolveNames(lrns);
|
||||||
const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL;
|
const pricingRecordsAttributes: PricingRecordAttributes[] = result.map((record: any) => {
|
||||||
if (!apiEndpoint) {
|
return record.attributes
|
||||||
console.error('NEXT_PUBLIC_COSMOS_API_URL environment variable not set');
|
});
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Axios to directly query the Cosmos transaction via REST API
|
return pricingRecordsAttributes;
|
||||||
const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`);
|
|
||||||
|
|
||||||
// Check if transaction exists and was successful
|
|
||||||
// The Cosmos API returns a tx_response object with a code field - 0 means success
|
|
||||||
if (response.data &&
|
|
||||||
response.data.tx_response &&
|
|
||||||
response.data.tx_response.code === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for successful transactions with code === undefined (some nodes report it this way)
|
|
||||||
if (response.data &&
|
|
||||||
response.data.tx_response &&
|
|
||||||
response.data.tx_response.code === undefined &&
|
|
||||||
response.data.tx_response.height) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also fallback to checking if the transaction has a height (was included in a block)
|
|
||||||
if (response.data &&
|
|
||||||
response.data.tx_response &&
|
|
||||||
response.data.tx_response.height &&
|
|
||||||
!response.data.tx_response.code) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to verify transaction:', error);
|
|
||||||
|
|
||||||
// If the API call fails, try checking a public explorer API as fallback
|
|
||||||
try {
|
|
||||||
// Try a different URL format that some RPC nodes might use
|
|
||||||
const rpcEndpoint = process.env.NEXT_PUBLIC_COSMOS_RPC_URL;
|
|
||||||
const fallbackResponse = await axios.get(`${rpcEndpoint}/tx?hash=0x${txHash}`);
|
|
||||||
|
|
||||||
if (fallbackResponse.data &&
|
|
||||||
fallbackResponse.data.result &&
|
|
||||||
(fallbackResponse.data.result.height ||
|
|
||||||
(fallbackResponse.data.result.tx_result &&
|
|
||||||
fallbackResponse.data.result.tx_result.code === 0))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error('Fallback verification also failed:', fallbackError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCostOfDeployment = async (): Promise<number> => {
|
||||||
|
const resolvedRecords = await resolvePricingRecordLrns([ALNT_COST_LRN, DEPLOYMENT_COST_LRN]);
|
||||||
|
console.log('resolvedRecords:', resolvedRecords);
|
||||||
|
|
||||||
|
// Find the ALNT price record (USD per ALNT)
|
||||||
|
const alntPriceRecord = resolvedRecords[0];
|
||||||
|
// Find the deployment cost record (ALNT cost for webapp-deployment)
|
||||||
|
const deploymentCostRecord = resolvedRecords[1];
|
||||||
|
|
||||||
|
if (!alntPriceRecord || !deploymentCostRecord) {
|
||||||
|
throw new Error('Required pricing records not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert strings to numbers for calculation
|
||||||
|
const alntPriceUsd = parseFloat(alntPriceRecord.amount); // USD per ALNT
|
||||||
|
const deploymentCostAlnt = parseFloat(deploymentCostRecord.amount); // ALNT required
|
||||||
|
|
||||||
|
// Calculate deployment cost in USD: (ALNT required) * (USD per ALNT)
|
||||||
|
return deploymentCostAlnt * alntPriceUsd;
|
||||||
|
}
|
||||||
|
267
src/services/solana.ts
Normal file
267
src/services/solana.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
import { Connection, PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
||||||
|
import {
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
createTransferInstruction,
|
||||||
|
createAssociatedTokenAccountInstruction,
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
getAssociatedTokenAddressSync
|
||||||
|
} from '@solana/spl-token';
|
||||||
|
import { WalletAdapter } from '@solana/wallet-adapter-base';
|
||||||
|
|
||||||
|
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_RECIPIENT_ADDRESS, 'SOLANA_TOKEN_RECIPIENT_ADDRESS is required');
|
||||||
|
|
||||||
|
const TOKEN_MINT = process.env.NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS;
|
||||||
|
const PAYMENT_RECEIVER_ADDRESS = process.env.NEXT_PUBLIC_SOLANA_TOKEN_RECIPIENT_ADDRESS;
|
||||||
|
|
||||||
|
// Helper function to find associated token address
|
||||||
|
async function findAssociatedTokenAddress(
|
||||||
|
walletAddress: PublicKey,
|
||||||
|
tokenMintAddress: PublicKey
|
||||||
|
): Promise<PublicKey> {
|
||||||
|
return PublicKey.findProgramAddressSync(
|
||||||
|
[
|
||||||
|
walletAddress.toBuffer(),
|
||||||
|
TOKEN_PROGRAM_ID.toBuffer(),
|
||||||
|
tokenMintAddress.toBuffer(),
|
||||||
|
],
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send native GOR payment
|
||||||
|
export async function sendNativeGorPayment(
|
||||||
|
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 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 mintPublicKey = new PublicKey(TOKEN_MINT);
|
||||||
|
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS);
|
||||||
|
|
||||||
|
console.log('Processing SPL token payment:', {
|
||||||
|
sender: senderPublicKey.toBase58(),
|
||||||
|
mint: mintPublicKey.toBase58(),
|
||||||
|
receiver: receiverPublicKey.toBase58(),
|
||||||
|
amount: tokenAmount.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const senderATA = await findAssociatedTokenAddress(
|
||||||
|
senderPublicKey,
|
||||||
|
mintPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const receiverATA = await findAssociatedTokenAddress(
|
||||||
|
receiverPublicKey,
|
||||||
|
mintPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Token accounts:', {
|
||||||
|
senderATA: senderATA.toBase58(),
|
||||||
|
receiverATA: receiverATA.toBase58(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const transaction = new Transaction();
|
||||||
|
|
||||||
|
// Check if accounts exist
|
||||||
|
const [senderATAInfo, receiverATAInfo] = await Promise.all([
|
||||||
|
connection.getAccountInfo(senderATA),
|
||||||
|
connection.getAccountInfo(receiverATA),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create receiver token account if it doesn't exist
|
||||||
|
if (!receiverATAInfo) {
|
||||||
|
console.log('Creating receiver token account');
|
||||||
|
transaction.add(
|
||||||
|
createAssociatedTokenAccountInstruction(
|
||||||
|
senderPublicKey,
|
||||||
|
receiverATA,
|
||||||
|
receiverPublicKey,
|
||||||
|
mintPublicKey
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sender token account if it doesn't exist
|
||||||
|
if (!senderATAInfo) {
|
||||||
|
console.log('Creating sender token account');
|
||||||
|
transaction.add(
|
||||||
|
createAssociatedTokenAccountInstruction(
|
||||||
|
senderPublicKey,
|
||||||
|
senderATA,
|
||||||
|
senderPublicKey,
|
||||||
|
mintPublicKey
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountToSend = BigInt(tokenAmount.toString());
|
||||||
|
|
||||||
|
// Add transfer instruction
|
||||||
|
transaction.add(
|
||||||
|
createTransferInstruction(
|
||||||
|
senderATA,
|
||||||
|
receiverATA,
|
||||||
|
senderPublicKey,
|
||||||
|
amountToSend
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set transaction details
|
||||||
|
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
|
||||||
|
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||||
|
transaction.feePayer = senderPublicKey;
|
||||||
|
|
||||||
|
console.log('Sending SPL token transaction...');
|
||||||
|
const signature = await wallet.sendTransaction(transaction, connection);
|
||||||
|
console.log('SPL token 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('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) {
|
||||||
|
console.error('Payment error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Payment failed'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get recipient address based on payment method
|
||||||
|
export function getRecipientAddress(paymentMethod: PaymentMethod): string {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentMethod.NAT_GOR:
|
||||||
|
// For native GOR payments, use the direct recipient address
|
||||||
|
return PAYMENT_RECEIVER_ADDRESS;
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
import { Window as KeplrWindow } from "@keplr-wallet/types";
|
// Payment method types
|
||||||
|
export enum PaymentMethod {
|
||||||
// extend the global Window interface to include Keplr
|
NAT_GOR = 'nat-gor',
|
||||||
declare global {
|
SPL_TOKEN = 'spl-token'
|
||||||
interface Window extends KeplrWindow {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
chainId: string;
|
chainId: string;
|
||||||
rpcEndpoint: string;
|
rpcEndpoint: string;
|
||||||
@ -12,32 +10,6 @@ export interface RegistryConfig {
|
|||||||
bondId: string;
|
bondId: string;
|
||||||
authority: string;
|
authority: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
fee: {
|
|
||||||
gas: string;
|
|
||||||
fees: string;
|
|
||||||
gasPrice: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionResponse {
|
|
||||||
hash: string;
|
|
||||||
status: 'success' | 'error';
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LaconicRecordData {
|
|
||||||
type: string;
|
|
||||||
version: string;
|
|
||||||
name: string;
|
|
||||||
application: string;
|
|
||||||
deployer: string;
|
|
||||||
dns: string;
|
|
||||||
meta: {
|
|
||||||
note: string;
|
|
||||||
repository: string;
|
|
||||||
repository_ref: string;
|
|
||||||
};
|
|
||||||
payment: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateRecordResponse {
|
export interface CreateRecordResponse {
|
||||||
@ -52,3 +24,37 @@ export interface CreateRecordResponse {
|
|||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface SolanaPaymentResult {
|
||||||
|
success: boolean;
|
||||||
|
transactionSignature?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
url: string;
|
||||||
|
onPaymentComplete: (txHash: string, paymentMethod: PaymentMethod) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface PaymentRequest {
|
||||||
|
paymentMethod: PaymentMethod;
|
||||||
|
amount: number; // in base units (lamports for native GOR, token base units for SPL)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LaconicTransferResult {
|
||||||
|
success: boolean;
|
||||||
|
transactionHash?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PricingRecordAttributes {
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
for: string;
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
1
src/utils/gorbagana.ts
Normal file
1
src/utils/gorbagana.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const IS_NAT_GOR_TRANSFER_ENABLED = process.env.NEXT_PUBLIC_ENABLE_NATIVE_GOR_TRANSFER !== "false";
|
154
src/utils/solana-verify.ts
Normal file
154
src/utils/solana-verify.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import BN from 'bn.js';
|
||||||
|
|
||||||
|
import { Connection, ParsedInstruction, ParsedTransactionWithMeta, PartiallyDecodedInstruction } from '@solana/web3.js';
|
||||||
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
|
||||||
|
import { getRecipientAddress } from '@/services/solana';
|
||||||
|
import { PaymentMethod } from '../types';
|
||||||
|
|
||||||
|
// Extract transaction info for native GOR transfers
|
||||||
|
const extractTxInfo = async (
|
||||||
|
parsedTx: ParsedTransactionWithMeta,
|
||||||
|
paymentMethod: PaymentMethod
|
||||||
|
): Promise<{ authority: string; amount: string; destination: string }> => {
|
||||||
|
|
||||||
|
if (!parsedTx) {
|
||||||
|
throw new Error('Transaction not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let transferInstruction: ParsedInstruction | PartiallyDecodedInstruction | undefined;
|
||||||
|
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentMethod.NAT_GOR:
|
||||||
|
// Look for system program transfer instruction
|
||||||
|
transferInstruction = parsedTx.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 = parsedTx.transaction.message.instructions.find(
|
||||||
|
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
||||||
|
throw new Error('SPL token transfer instruction not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = transferInstruction.parsed;
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
connection: Connection,
|
||||||
|
parsedTx: ParsedTransactionWithMeta,
|
||||||
|
expectedAmount: BN,
|
||||||
|
paymentMethod: PaymentMethod,
|
||||||
|
): Promise<{
|
||||||
|
valid: boolean,
|
||||||
|
reason?: string,
|
||||||
|
amount?: string,
|
||||||
|
sender?: string
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
// TODO: Check if provided signature is already used
|
||||||
|
|
||||||
|
// Fetch transaction details
|
||||||
|
if (!parsedTx) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'Transaction not found on Solana blockchain'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if transaction was successful
|
||||||
|
if (parsedTx.meta?.err) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `Transaction failed: ${JSON.stringify(parsedTx.meta.err)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check transaction timestamp (5-minute window)
|
||||||
|
const txTimestamp = parsedTx.blockTime ? new Date(parsedTx.blockTime * 1000) : null;
|
||||||
|
if (!txTimestamp) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'Transaction timestamp not available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const timeDiffMs = now.getTime() - txTimestamp.getTime();
|
||||||
|
const timeWindowMs = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
if (timeDiffMs > timeWindowMs) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `Transaction is older than 5 minutes (${Math.round(timeDiffMs / 60000)} minutes old)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract transaction info based on payment method
|
||||||
|
const transferInfo = await extractTxInfo(parsedTx, paymentMethod);
|
||||||
|
const amount = transferInfo.amount;
|
||||||
|
const authority = transferInfo.authority;
|
||||||
|
const destination = transferInfo.destination;
|
||||||
|
|
||||||
|
// Verify amount using BN comparison
|
||||||
|
const transactionAmount = new BN(amount);
|
||||||
|
if (transactionAmount.lt(expectedAmount)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `Payment amount (${amount}) is less than required (${expectedAmount.toString()})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify recipient address
|
||||||
|
const expectedRecipientAddress = getRecipientAddress(paymentMethod);
|
||||||
|
|
||||||
|
if (destination !== expectedRecipientAddress) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
amount,
|
||||||
|
sender: authority
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying Solana payment:', error);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
@ -22,6 +22,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.mjs"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user