fin
This commit is contained in:
parent
43e4b38e96
commit
f95bde74f7
85
README.md
85
README.md
@ -8,6 +8,8 @@ A simple Next.js frontend that allows users to pay in ATOM cryptocurrency (using
|
||||
- URL validation and submission
|
||||
- Transaction verification
|
||||
- Laconic Registry record creation using official `@cerc-io/registry-sdk`
|
||||
- Automatic salt addition to DNS names to prevent collisions
|
||||
- Error handling and validation throughout the application flow
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@ -30,6 +32,7 @@ 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
|
||||
- `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")
|
||||
|
||||
Server-side:
|
||||
- `REGISTRY_CHAIN_ID` - The chain ID for the Laconic Registry
|
||||
@ -74,7 +77,10 @@ npm start
|
||||
3. User completes payment in ATOM to a specified address
|
||||
4. The application verifies the transaction using the Cosmos RPC
|
||||
5. The application calls a server-side API route which creates records in the Laconic Registry
|
||||
6. The record includes the URL, transaction hash, and other metadata
|
||||
6. The server generates a unique DNS name by adding a random salt to prevent name collisions
|
||||
7. Two records are created in the Laconic Registry:
|
||||
- An ApplicationRecord containing metadata about the URL
|
||||
- An ApplicationDeploymentRequest linking the URL, DNS, and payment transaction
|
||||
|
||||
### Architecture
|
||||
|
||||
@ -85,12 +91,89 @@ This application uses a hybrid client/server approach:
|
||||
|
||||
This architecture allows us to keep sensitive keys secure on the server side while providing a responsive user experience.
|
||||
|
||||
### Resource Name Formats
|
||||
|
||||
#### DNS Name Format
|
||||
|
||||
The DNS names are generated with the following format:
|
||||
```
|
||||
{sanitized-url-name}-{short-commit-hash}-{random-salt}{domain-suffix}
|
||||
```
|
||||
|
||||
For example:
|
||||
- Basic DNS: `github-abc123-xyz789`
|
||||
- With domain suffix: `github-abc123-xyz789.example.com`
|
||||
|
||||
The random salt ensures that each deployment request has a unique DNS name, even if the same URL is deployed multiple times or by different users. The optional domain suffix allows the application to display a full domain name to users.
|
||||
|
||||
#### Laconic Resource Name (LRN) Format
|
||||
|
||||
The Laconic Resource Names (LRNs) are generated with the following format:
|
||||
```
|
||||
lrn://{authority}/applications/{app-name}-{short-commit-hash}-{random-salt}
|
||||
```
|
||||
|
||||
For example: `lrn://atom/applications/github-abc123-xyz789`
|
||||
|
||||
Including the commit hash and salt in the LRN ensures that each application record has a unique identifier, consistently matching the DNS naming pattern.
|
||||
|
||||
## Reference Files
|
||||
|
||||
This application was built with reference to:
|
||||
- `snowballtools-base/packages/backend/src/registry.ts`
|
||||
- `hosted-frontends/deploy-atom.sh`
|
||||
|
||||
## 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
|
||||
|
||||
- You may see a deprecated Buffer() warning during build. This comes from dependencies in the registry-sdk. This doesn't affect functionality.
|
||||
- If using a custom Cosmos chain, ensure that your RPC endpoint supports CORS for client-side requests.
|
||||
- The Keplr wallet integration requires HTTPS in production environments.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Keplr Wallet Issues
|
||||
|
||||
- **Keplr not detecting**: Install the Keplr browser extension and refresh the page.
|
||||
- **Chain not found in Keplr**: The application will attempt to suggest the chain to Keplr, but if that fails, you may need to manually add the chain in your Keplr wallet settings.
|
||||
|
||||
### Laconic Registry Issues
|
||||
|
||||
- **Failed to create record**: Check that your REGISTRY_USER_KEY and REGISTRY_BOND_ID are correctly set.
|
||||
- **Transaction verification errors**: Ensure your COSMOS_RPC_URL is accessible and returns correct transaction data.
|
||||
|
@ -118,7 +118,25 @@ const registryTransactionWithRetry = async (
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { url, txHash } = await request.json();
|
||||
// First check if the request body is valid JSON
|
||||
let url, txHash;
|
||||
try {
|
||||
const body = await request.json();
|
||||
url = body.url;
|
||||
txHash = body.txHash;
|
||||
|
||||
if (!url || !txHash) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Missing required fields: url and txHash are required'
|
||||
}, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: 'Invalid JSON in request body'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate required environment variables
|
||||
const requiredEnvVars = [
|
||||
@ -133,9 +151,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
for (const envVar of requiredEnvVars) {
|
||||
if (!process.env[envVar]) {
|
||||
console.error(`Missing environment variable: ${envVar}`);
|
||||
return NextResponse.json({
|
||||
status: 'error',
|
||||
message: `Missing environment variable: ${envVar}`
|
||||
message: `Server configuration error: Missing environment variable: ${envVar}`
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -155,9 +174,17 @@ export async function POST(request: NextRequest) {
|
||||
// Sanitize the app name to ensure it's DNS-compatible (only alphanumeric and dashes)
|
||||
const sanitizedAppName = appName.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
||||
|
||||
// Create DNS name in format: app_name-shortcommithash
|
||||
const dnsName = `${sanitizedAppName}-${shortHash}`;
|
||||
console.log(`DNS name: ${dnsName} (sanitized from: ${appName})`);
|
||||
// Generate a random salt (6 alphanumeric characters) to prevent name collisions
|
||||
const generateSalt = (): string => {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
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
|
||||
let cleanDnsName = dnsName
|
||||
@ -166,10 +193,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Ensure DNS name is valid (63 chars max per label, all lowercase, starts with a letter)
|
||||
if (cleanDnsName.length > 63) {
|
||||
// If too long, truncate but keep the commit hash part
|
||||
const hashPart = `-${shortHash}`;
|
||||
const maxAppNameLength = 63 - hashPart.length;
|
||||
cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + hashPart;
|
||||
// If too long, truncate but preserve both the commit hash and salt parts
|
||||
const suffixPart = `-${shortHash}-${salt}`;
|
||||
const maxAppNameLength = 63 - suffixPart.length;
|
||||
cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + suffixPart;
|
||||
}
|
||||
|
||||
// If the DNS name ended up empty (unlikely) or doesn't start with a letter (possible),
|
||||
@ -178,7 +205,7 @@ export async function POST(request: NextRequest) {
|
||||
cleanDnsName = `app-${cleanDnsName}`;
|
||||
}
|
||||
|
||||
console.log(`Final DNS name: ${cleanDnsName}`);
|
||||
console.log(`Final DNS name with salt: ${cleanDnsName}`);
|
||||
|
||||
// Set up Registry config
|
||||
const config = {
|
||||
@ -212,8 +239,9 @@ export async function POST(request: NextRequest) {
|
||||
{ chainId: config.chainId, gasPrice }
|
||||
);
|
||||
|
||||
// Create LRN for the application
|
||||
const lrn = `lrn://${config.authority}/applications/${appName}`;
|
||||
// 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
|
||||
const timestamp = new Date().toUTCString();
|
||||
@ -222,7 +250,7 @@ export async function POST(request: NextRequest) {
|
||||
console.log('Step 1: Publishing ApplicationRecord...');
|
||||
const applicationRecord = {
|
||||
type: 'ApplicationRecord',
|
||||
name: appName,
|
||||
name: `${appName}-${shortHash}-${salt}`, // Include commit hash and salt in the record name
|
||||
version: '1.0.0',
|
||||
app_type: 'webapp',
|
||||
repository: [repoUrl],
|
||||
@ -232,8 +260,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Create fee for transaction directly
|
||||
const fee = {
|
||||
amount: [{ denom: 'alnt', amount: '900000' }],
|
||||
gas: '900000',
|
||||
amount: [{ denom: 'alnt', amount: process.env.REGISTRY_FEES?.replace('alnt', '') || '900000' }],
|
||||
gas: process.env.REGISTRY_GAS || '900000',
|
||||
};
|
||||
|
||||
console.log('Application record data:', applicationRecord);
|
||||
@ -312,8 +340,8 @@ export async function POST(request: NextRequest) {
|
||||
const deploymentRequestData = {
|
||||
type: 'ApplicationDeploymentRequest',
|
||||
version: '1.0.0',
|
||||
name: appName,
|
||||
application: lrn,
|
||||
name: `${appName}-${shortHash}-${salt}`, // Update name to match application record
|
||||
application: lrn, // LRN already includes commit hash and salt
|
||||
deployer: deployerLrn,
|
||||
dns: cleanDnsName,
|
||||
config: {
|
||||
|
@ -14,7 +14,7 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ATOM Deploy - Laconic Registry",
|
||||
title: "Deploy Frontends using ATOM and Laconic",
|
||||
description: "Deploy URLs to Laconic Registry using ATOM payments",
|
||||
};
|
||||
|
||||
|
@ -99,7 +99,7 @@ export default function Home() {
|
||||
<div style={{ background: 'var(--card-bg)', borderColor: 'var(--card-border)' }}
|
||||
className="max-w-xl w-full p-8 rounded-xl shadow-lg border">
|
||||
<h1 className="text-2xl font-bold mb-8 text-center" style={{ color: 'var(--foreground)' }}>
|
||||
ATOM Deploy - Laconic Registry
|
||||
Deploy Frontends with ATOM and Laconic
|
||||
</h1>
|
||||
|
||||
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
|
||||
|
@ -23,7 +23,25 @@ export default function PaymentModal({
|
||||
// Get recipient address from environment variables
|
||||
const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
|
||||
|
||||
// Validate amount on change
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setAmount(value);
|
||||
|
||||
// Clear error when user types
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayment = async () => {
|
||||
// Validate amount before sending
|
||||
const parsedAmount = parseFloat(amount);
|
||||
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
||||
setError('Please enter a valid positive amount');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
@ -78,7 +96,7 @@ export default function PaymentModal({
|
||||
min="0.001"
|
||||
step="0.001"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
onChange={handleAmountChange}
|
||||
className="w-full p-3 pr-12 rounded-md"
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
|
@ -27,6 +27,8 @@ export default function StatusDisplay({
|
||||
shortCommitHash,
|
||||
error,
|
||||
}: StatusDisplayProps) {
|
||||
// Get domain suffix from environment variable
|
||||
const domainSuffix = process.env.NEXT_PUBLIC_DOMAIN_SUFFIX || '';
|
||||
if (status === 'idle') return null;
|
||||
|
||||
const StatusBadge = ({ type }: { type: 'verifying' | 'creating' | 'success' | 'error' }) => {
|
||||
@ -115,23 +117,20 @@ export default function StatusDisplay({
|
||||
<h3 className="font-semibold mb-2 text-lg">
|
||||
Successfully deployed
|
||||
{appName && <span> {appName}</span>}
|
||||
{dns && <span> as {dns}</span>}
|
||||
</h3>
|
||||
{repoUrl && (
|
||||
<p className="mb-1 text-sm">
|
||||
<span className="font-medium">Repository:</span> {repoUrl}
|
||||
{shortCommitHash && <span> @ {shortCommitHash}</span>}
|
||||
{(!shortCommitHash && commitHash) && <span> @ {commitHash.substring(0, 7)}</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{txHash && <InfoItem label="Transaction Hash" value={txHash} />}
|
||||
{appRecordId && <InfoItem label="Application Record ID" value={appRecordId} />}
|
||||
{recordId && <InfoItem label="Deployment Request Record ID" value={recordId} />}
|
||||
{txHash && <InfoItem label="ATOM Payment Transaction Hash" value={txHash} />}
|
||||
{appRecordId && <InfoItem label="Laconic Application Record ID" value={appRecordId} />}
|
||||
{recordId && <InfoItem label="Laconic Deployment Request Record ID" value={recordId} />}
|
||||
{lrn && <InfoItem label="Laconic Resource Name (LRN)" value={lrn} />}
|
||||
{dns && <InfoItem label="Deployment DNS" value={dns} />}
|
||||
{dns && <InfoItem label="Deployment DNS" value={domainSuffix ? `${dns}${domainSuffix}` : dns} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -14,18 +14,41 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate URL
|
||||
// Trim the URL to remove any whitespace
|
||||
const trimmedUrl = url.trim();
|
||||
|
||||
if (!trimmedUrl) {
|
||||
setError('Please enter a URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const parsedUrl = new URL(trimmedUrl);
|
||||
|
||||
// Check for protocol
|
||||
if (!parsedUrl.protocol.startsWith('http')) {
|
||||
setError('URL must use HTTP or HTTPS protocol');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for hostname
|
||||
if (!parsedUrl.hostname || parsedUrl.hostname.length < 3) {
|
||||
setError('URL must contain a valid hostname');
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic sanity check for common invalid URLs
|
||||
if (parsedUrl.href === 'http://localhost' || parsedUrl.href === 'https://localhost') {
|
||||
setError('Please enter a valid public URL, not localhost');
|
||||
return;
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
setError('');
|
||||
onSubmit(url);
|
||||
onSubmit(trimmedUrl);
|
||||
} catch (_) {
|
||||
setError('Please enter a valid URL');
|
||||
setError('Please enter a valid URL (e.g., https://example.com)');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -9,12 +9,74 @@ export const connectKeplr = async (): Promise<string | 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(process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4');
|
||||
const offlineSigner = window.keplr.getOfflineSigner(process.env.NEXT_PUBLIC_COSMOS_CHAIN_ID || 'cosmoshub-4');
|
||||
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);
|
||||
@ -35,9 +97,62 @@ export const sendAtomPayment = async (
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -59,6 +174,13 @@ export const sendAtomPayment = async (
|
||||
|
||||
// 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)
|
||||
@ -75,16 +197,55 @@ export const sendAtomPayment = async (
|
||||
}
|
||||
);
|
||||
|
||||
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: error instanceof Error ? error.message : 'Unknown 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'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user