@enclavemoney/enclave-wallet-sdk
Version:
A simple enclave wallet SDK for React applications
288 lines (264 loc) • 8.56 kB
text/typescript
// Add this mapping at the top (after TOKEN_NETWORKS)
export const NETWORK_NAME_TO_CHAIN_ID: Record<string, number> = {
Arbitrum: 42161,
Optimism: 10,
Base: 8453,
Solana: 792703809,
Bitcoin: 8253038,
Sonic: 146,
Ethereum: 1,
Unichain: 130,
Avalanche: 43114,
Polygon: 137,
BNB: 56,
};
export function getNetworkName(chainId: string | number): string {
const chainIdNumber =
typeof chainId === "string" ? parseInt(chainId) : chainId;
const networkName = Object.entries(NETWORK_NAME_TO_CHAIN_ID).find(
([_, id]) => id === chainIdNumber
)?.[0];
return networkName || chainId.toString();
}
export const NETWORK_LOGO_URLS: Record<string, string> = {
42161: "https://storage.googleapis.com/enclave_assets/arb-logo.svg",
10: "https://storage.googleapis.com/enclave_assets/optimism-logo.svg",
8453: "https://storage.googleapis.com/enclave_assets/base.svg",
792703809: "https://storage.googleapis.com/enclave_assets/sol.png",
8253038:
"https://assets.coingecko.com/coins/images/1/standard/bitcoin.png?1696501400",
56: "https://assets.coingecko.com/coins/images/825/standard/bnb-icon2_2x.png?1696501970",
137: "https://assets.coingecko.com/coins/images/4713/standard/polygon.png?1698233745",
43114:
"https://assets.coingecko.com/coins/images/12559/standard/Avalanche_Circle_RedWhite_Trans.png?1696512369",
130: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg",
146: "https://assets.coingecko.com/coins/images/38108/standard/200x200_Sonic_Logo.png?1734679256",
1: "https://media.socket.tech/tokens/all/ETH",
};
export function formatAmount(amount: string) {
const num = parseFloat(amount);
const decimalPlaces = amount.split(".")[1]?.length || 0;
return decimalPlaces > 6 ? num.toFixed(6) : amount;
}
// Simple debounce implementation
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
/**
* Validates and sanitizes numeric input to prevent invalid formats
* @param value The input value to validate
* @param maxDecimals Maximum number of decimal places allowed (optional)
* @returns The sanitized value or empty string if invalid
*/
export function validateNumericInput(
value: string,
maxDecimals?: number
): string {
if (!value) return "";
// Remove any non-digit, non-decimal point characters
// This prevents things like "1+1", "1--", etc.
let sanitized = value.replace(/[^0-9.]/g, "");
// Handle multiple decimal points - keep only the first one
const parts = sanitized.split(".");
if (parts.length > 2) {
sanitized = parts[0] + "." + parts.slice(1).join("");
}
// Prevent starting with decimal point for better UX
if (sanitized.startsWith(".")) {
sanitized = "0" + sanitized;
}
// Prevent multiple leading zeros (but allow single zero before decimal)
if (
sanitized.length > 1 &&
sanitized.startsWith("0") &&
!sanitized.startsWith("0.")
) {
sanitized = sanitized.replace(/^0+/, "0");
}
// Enforce maximum decimal places if specified
if (maxDecimals !== undefined && sanitized.includes(".")) {
const [integerPart, decimalPart] = sanitized.split(".");
if (decimalPart.length > maxDecimals) {
sanitized = integerPart + "." + decimalPart.slice(0, maxDecimals);
}
}
// Final validation - ensure it's a valid number format
if (sanitized && (isNaN(Number(sanitized)) || sanitized.includes(".."))) {
return "";
}
return sanitized;
}
/**
* Handles keydown events to prevent invalid characters from being typed
* @param e The keyboard event
* @param currentValue The current input value
* @param maxDecimals Maximum number of decimal places allowed (optional)
*/
export function handleNumericKeyDown(
e: React.KeyboardEvent<HTMLInputElement>,
currentValue: string,
maxDecimals?: number
) {
// Allow basic navigation and editing keys
const allowedKeys = [
"Backspace",
"Delete",
"Tab",
"Escape",
"Enter",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
"PageUp",
"PageDown",
];
// Allow Ctrl/Cmd combinations (copy, paste, select all, etc.)
if (e.ctrlKey || e.metaKey) {
return;
}
// Allow allowed keys
if (allowedKeys.includes(e.key)) {
return;
}
// Allow digits, but check decimal limit if applicable
if (/^[0-9]$/.test(e.key)) {
// If maxDecimals is specified and we're after a decimal point, check limit
if (maxDecimals !== undefined && currentValue.includes(".")) {
const decimalPart = currentValue.split(".")[1];
if (decimalPart && decimalPart.length >= maxDecimals) {
// Check if cursor is in the decimal part
const target = e.target as HTMLInputElement;
const cursorPosition = target.selectionStart || 0;
const decimalIndex = currentValue.indexOf(".");
// If cursor is after decimal and we've reached the limit, prevent input
if (
cursorPosition > decimalIndex &&
decimalPart.length >= maxDecimals
) {
e.preventDefault();
return;
}
}
}
return;
}
// Allow decimal point only if there isn't one already
if (e.key === "." && !currentValue.includes(".")) {
return;
}
// Prevent all other characters including +, -, e, E, etc.
e.preventDefault();
}
export const availableNetworks = [
{
name: "Solana",
icon: "https://storage.googleapis.com/enclave_assets/sol.png",
},
{
name: "Arbitrum",
icon: "https://storage.googleapis.com/enclave_assets/arb-logo.svg",
},
{
name: "Optimism",
icon: "https://storage.googleapis.com/enclave_assets/optimism-logo.svg",
},
{
name: "Base",
icon: "https://storage.googleapis.com/enclave_assets/base.svg",
},
{
name: "BNB",
icon: "https://assets.coingecko.com/coins/images/825/standard/bnb-icon2_2x.png?1696501970",
},
{
name: "Polygon",
icon: "https://assets.coingecko.com/coins/images/4713/standard/polygon.png?1698233745",
},
{
name: "Avalanche",
icon: "https://assets.coingecko.com/coins/images/12559/standard/Avalanche_Circle_RedWhite_Trans.png?1696512369",
},
{
name: "Unichain",
icon: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg",
},
{
name: "Sonic",
icon: "https://assets.coingecko.com/coins/images/38108/standard/200x200_Sonic_Logo.png?1734679256",
},
{
name: "Ethereum",
icon: "https://media.socket.tech/tokens/all/ETH",
},
];
export enum ProtocolProvider {
LIFI = "LIFI",
RELAY = "RELAY",
BUNGEE = "BUNGEE",
GARDEN = "GARDEN",
ONEINCH = "1INCH",
INCOMING = "INCOMING",
CCTP_V1 = "CCTP_V1",
CCTP_V2_FAST = "CCTP_V2_FAST",
CCTP_V2_STANDARD = "CCTP_V2_SLOW",
}
/**
* Gets the appropriate icon URL for a protocol provider
* @param provider The protocol provider
* @returns The icon URL for the provider
*/
export function getProviderIcon(
provider: ProtocolProvider | string
): string | null {
switch (provider) {
case ProtocolProvider.LIFI:
return "https://storage.googleapis.com/enclave_assets/lifi.png";
case ProtocolProvider.BUNGEE:
return "https://storage.googleapis.com/enclave_assets/bungee.png";
case ProtocolProvider.RELAY:
return "https://storage.googleapis.com/enclave_assets/relay.png";
case ProtocolProvider.CCTP_V1:
case ProtocolProvider.CCTP_V2_FAST:
case ProtocolProvider.CCTP_V2_STANDARD:
return "https://storage.googleapis.com/enclave_assets/circle.png";
default:
return null;
}
}
/**
* Gets the human-readable name for a protocol provider
* @param provider The protocol provider
* @returns The display name for the provider
*/
export function getProviderName(provider: ProtocolProvider | string): string {
switch (provider) {
case ProtocolProvider.BUNGEE:
return "Bungee";
case ProtocolProvider.RELAY:
return "Relay";
case ProtocolProvider.CCTP_V1:
return "Circle CCTP V1";
case ProtocolProvider.CCTP_V2_FAST:
return "Circle CCTP V2 Fast";
case ProtocolProvider.CCTP_V2_STANDARD:
return "Circle CCTP V2 Standard";
case ProtocolProvider.LIFI:
return "LiFi";
case ProtocolProvider.GARDEN:
return "Garden";
case ProtocolProvider.ONEINCH:
return "1inch";
default:
return provider || "Unknown Provider";
}
}