gor-deploy/src/services/keplr.ts
2025-05-05 16:47:39 -04:00

260 lines
8.2 KiB
TypeScript

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();
};