260 lines
8.2 KiB
TypeScript
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();
|
|
}; |