diff --git a/packages/app/package-lock.json b/packages/app/package-lock.json index 6c90506..36991c4 100644 --- a/packages/app/package-lock.json +++ b/packages/app/package-lock.json @@ -13,6 +13,7 @@ "@fontsource/inter-tight": "^5.2.5", "@fontsource/space-grotesk": "^5.2.6", "@number-flow/react": "^0.5.9", + "@pwrjs/core": "^0.12.8", "@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", @@ -429,6 +430,15 @@ "node": ">=6" } }, + "node_modules/@dashlane/pqc-sign-falcon-512-browser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@dashlane/pqc-sign-falcon-512-browser/-/pqc-sign-falcon-512-browser-1.0.0.tgz", + "integrity": "sha512-eMiytG1LDPsxrB1UfkGZb9+fzfYY3hHoiPvJ7e6k5ULVXttKQsOoor0NIQdAiaoS9blagv//mkX0xq2YmmfB6w==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@ecies/ciphers": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.3.tgz", @@ -4378,6 +4388,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@pwrjs/core": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/@pwrjs/core/-/core-0.12.9.tgz", + "integrity": "sha512-a2Ft33XRK3H2rFV/2swgrFojRr+5Ai1QY+m+syzXyclv9EfcdJ3L48pjdTLO+U4gYVSTwhNtNB3IgC3krVhzUg==", + "license": "ISC", + "dependencies": { + "@dashlane/pqc-sign-falcon-512-browser": "^1.0.0", + "@noble/hashes": "^1.7.1", + "bignumber.js": "^9.1.2", + "bip39": "^3.1.0", + "rust-falcon": "^0.2.6" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", @@ -10019,6 +10042,15 @@ "url": "https://opencollective.com/bigjs" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -10031,6 +10063,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, "node_modules/bn.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", @@ -16359,6 +16400,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rust-falcon": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/rust-falcon/-/rust-falcon-0.2.6.tgz", + "integrity": "sha512-Jjnjj9PcaiM3mKdJ3Cap91D/hAedK/tWa6r1TJegw29fkZu7jwHGyp6in2SpEyQUNs1V9oUFFxAtq7Hp4OWTVw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", diff --git a/packages/app/package.json b/packages/app/package.json index fbdac00..c39c7eb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -15,6 +15,7 @@ "@fontsource/inter-tight": "^5.2.5", "@fontsource/space-grotesk": "^5.2.6", "@number-flow/react": "^0.5.9", + "@pwrjs/core": "^0.12.8", "@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", diff --git a/packages/app/public/logos/pwrlabs.avif b/packages/app/public/logos/pwrlabs.avif new file mode 100644 index 0000000..685e068 Binary files /dev/null and b/packages/app/public/logos/pwrlabs.avif differ diff --git a/packages/app/src/chain/networks.ts b/packages/app/src/chain/networks.ts index 2f4cbb1..3d084ba 100644 --- a/packages/app/src/chain/networks.ts +++ b/packages/app/src/chain/networks.ts @@ -6,6 +6,7 @@ import { fuelChains, type FuelChainConfig } from "@/fuel/config"; import { aptosChains, type AptosChainConfig } from "@/aptos/config"; import { soonChains, type SoonChainConfig } from "@/soon/config"; import { starknetChains, type StarknetChainConfig } from "@/starknet/config"; +import { pwrChains, type PwrChainConfig } from "@/pwr/config"; export interface ChainConfig extends Chain { testnet: boolean; @@ -135,11 +136,11 @@ export const evmChains = [ seiMainnet_ ]; -// All chains (EVM + Solana + Fuel + Aptos + SOON + Starknet) -export const allChains = [...evmChains, ...solanaChains, ...fuelChains, ...aptosChains, ...soonChains, ...starknetChains]; +// All chains (EVM + Solana + Fuel + Aptos + SOON + Starknet + PWR) +export const allChains = [...evmChains, ...solanaChains, ...fuelChains, ...aptosChains, ...soonChains, ...starknetChains, ...pwrChains]; // Backward compatibility - rename raceChains to evmChains export const raceChains = evmChains; // Export chain type union -export type AnyChainConfig = ChainConfig | SolanaChainConfig | FuelChainConfig | AptosChainConfig | SoonChainConfig | StarknetChainConfig; \ No newline at end of file +export type AnyChainConfig = ChainConfig | SolanaChainConfig | FuelChainConfig | AptosChainConfig | SoonChainConfig | StarknetChainConfig | PwrChainConfig; \ No newline at end of file diff --git a/packages/app/src/components/EmbeddedWallet.tsx b/packages/app/src/components/EmbeddedWallet.tsx index 180cfc7..4332c52 100644 --- a/packages/app/src/components/EmbeddedWallet.tsx +++ b/packages/app/src/components/EmbeddedWallet.tsx @@ -7,6 +7,7 @@ import { useFuelEmbeddedWallet } from "@/hooks/useFuelEmbeddedWallet"; import { useAptosEmbeddedWallet } from "@/hooks/useAptosEmbeddedWallet"; import { useSoonEmbeddedWallet } from "@/hooks/useSoonEmbeddedWallet"; import { useStarknetEmbeddedWallet } from "@/hooks/useStarknetEmbeddedWallet"; +import { usePwrEmbeddedWallet } from "@/hooks/usePwrEmbeddedWallet"; import { CopyIcon, Eye, EyeOff } from "lucide-react"; import { useState } from "react"; @@ -17,21 +18,23 @@ export function EmbeddedWallet() { const { address: aptosAddress, privateKey: aptosPrivateKey, isReady: aptosReady } = useAptosEmbeddedWallet(); const { publicKey: soonPublicKey, secret: soonSecret, isReady: soonReady } = useSoonEmbeddedWallet(); const { starknetprivateKey, starknetaccount, progress } = useStarknetEmbeddedWallet(); - const [copied, setCopied] = useState<"address" | "key" | "sol-address" | "sol-key" | "fuel-address" | "fuel-key" | "aptos-address" | "aptos-key" | "soon-address" | "soon-key" | "starknet-address" | "starknet-key" | null>(null); + const { address: pwrAddress, seedPhrase: pwrSeedPhrase, isReady: pwrReady } = usePwrEmbeddedWallet(); + const [copied, setCopied] = useState<"address" | "key" | "sol-address" | "sol-key" | "fuel-address" | "fuel-key" | "aptos-address" | "aptos-key" | "soon-address" | "soon-key" | "starknet-address" | "starknet-key" | "pwr-address" | "pwr-key" | null>(null); const [showKey, setShowKey] = useState(false); const [showSolanaKey, setShowSolanaKey] = useState(false); const [showFuelKey, setShowFuelKey] = useState(false); const [showAptosKey, setShowAptosKey] = useState(false); const [showSoonKey, setShowSoonKey] = useState(false); const [showStarknetKey, setShowStarknetKey] = useState(false); + const [showPwrKey, setShowPwrKey] = useState(false); - const copyToClipboard = (text: string, type: "address" | "key" | "sol-address" | "sol-key" | "fuel-address" | "fuel-key" | "aptos-address" | "aptos-key" | "soon-address" | "soon-key" | "starknet-address" | "starknet-key") => { + const copyToClipboard = (text: string, type: "address" | "key" | "sol-address" | "sol-key" | "fuel-address" | "fuel-key" | "aptos-address" | "aptos-key" | "soon-address" | "soon-key" | "starknet-address" | "starknet-key" | "pwr-address" | "pwr-key") => { navigator.clipboard.writeText(text); setCopied(type); setTimeout(() => setCopied(null), 2000); }; - if (!account || !privateKey || !solanaReady || !fuelReady || !aptosReady || !soonReady) { + if (!account || !privateKey || !solanaReady || !fuelReady || !aptosReady || !soonReady || !pwrReady) { return ( @@ -348,6 +351,54 @@ export function EmbeddedWallet() { )} + + {/* Separator */} + + + {/* PWR Wallet Section */} + {pwrAddress && pwrSeedPhrase && ( + + PWR Wallet + + Wallet Address + + + {pwrAddress} + + copyToClipboard(pwrAddress, "pwr-address")} + > + {copied === "pwr-address" ? "Copied!" : } + + + + + + Seed Phrase (Do not share!) + + + {showPwrKey ? pwrSeedPhrase : "••••••••••••••••••••"} + + setShowPwrKey(!showPwrKey)} + > + {showPwrKey ? : } + + copyToClipboard(pwrSeedPhrase, "pwr-key")} + > + {copied === "pwr-key" ? "Copied!" : } + + + + + )} ); diff --git a/packages/app/src/hooks/useChainRace.ts b/packages/app/src/hooks/useChainRace.ts index 66296c1..ba02c7a 100644 --- a/packages/app/src/hooks/useChainRace.ts +++ b/packages/app/src/hooks/useChainRace.ts @@ -27,6 +27,8 @@ import { saveRaceResults } from "@/lib/api"; import { WalletUnlocked, bn, Provider, type TransactionRequest, ScriptTransactionRequest, type Coin, ResolvedOutput, OutputChange } from "fuels"; import { useStarknetEmbeddedWallet } from "./useStarknetEmbeddedWallet"; import { StarknetChainConfig } from "@/starknet/config"; +import { usePwrEmbeddedWallet } from "./usePwrEmbeddedWallet"; +import { PwrChainConfig } from "@/pwr/config"; import { Erc20Abi } from "../util/erc20abi"; import { STRK_ADDRESS } from "../util/erc20Contract"; import { @@ -123,6 +125,10 @@ function isStarknetChain(chain: AnyChainConfig): chain is StarknetChainConfig { return chain.id === "starknet-testnet" || chain.id === "starknet-mainnet"; } +function isPwrChain(chain: AnyChainConfig): chain is PwrChainConfig { + return chain.id === "pwr-testnet" || chain.id === "pwr-mainnet"; +} + // Helper function to get fallback RPC endpoints for Solana function getSolanaFallbackEndpoints(chain: SolanaChainConfig): string[] { const fallbackEndpoints = [ @@ -148,6 +154,7 @@ export function useChainRace() { const { account: aptosAccount, address: aptosAddress, isReady: aptosReady } = useAptosEmbeddedWallet(); const { publicKey: soonPublicKey, keypair: soonKeypair } = useSoonEmbeddedWallet(); const { starknetprivateKey, starknetaccount, starknetisReady } = useStarknetEmbeddedWallet(); + const { wallet: pwrWallet, address: pwrAddress, isReady: pwrReady } = usePwrEmbeddedWallet(); const [status, setStatus] = useState("idle"); const [balances, setBalances] = useState([]); const [results, setResults] = useState([]); @@ -256,6 +263,8 @@ export function useChainRace() { if (layerFilter !== 'L2') return false; } else if (isStarknetChain(chain)) { if (layerFilter !== 'L2') return false; + } else if (isPwrChain(chain)) { + if (chain.layer !== layerFilter) return false; } else { // For Solana chains, we'll consider them as L1 for filtering purposes if (layerFilter !== 'L1') return false; @@ -279,12 +288,15 @@ export function useChainRace() { const isTestnet = chain.testnet; if (networkFilter === 'Testnet' && !isTestnet) return false; if (networkFilter === 'Mainnet' && isTestnet) return false; - }else if (isStarknetChain(chain)) { + } else if (isStarknetChain(chain)) { const isTestnet = chain.testnet; if (networkFilter === 'Testnet' && !isTestnet) return false; if (networkFilter === 'Mainnet' && isTestnet) return false; - } - else { + } else if (isPwrChain(chain)) { + const isTestnet = chain.testnet; + if (networkFilter === 'Testnet' && !isTestnet) return false; + if (networkFilter === 'Mainnet' && isTestnet) return false; + } else { // For Solana chains, check if it's mainnet or testnet based on the id const isMainnet = chain.id === 'solana-mainnet'; if (networkFilter === 'Mainnet' && !isMainnet) return false; @@ -501,6 +513,34 @@ export function useChainRace() { balance, hasBalance, }; + } else if (isPwrChain(chain)) { + // PWR chain balance check + if (!pwrReady || !pwrWallet) { + return { + chainId, + balance: BigInt(0), + hasBalance: false, + error: "PWR wallet still loading..." + }; + } + + try { + const balanceResponse = await pwrWallet.getBalance(); + + // Convert to bigint for consistency + balance = BigInt(balanceResponse); + // Minimum balance threshold: 0.001 PWR (assuming 18 decimals like ETH) + const hasBalance = balance > BigInt(1e15); + + return { + chainId, + balance, + hasBalance, + }; + } catch (error) { + console.error(`PWR balance check failed for ${chain.id}:`, error); + throw error; + } } else { throw new Error(`Unsupported chain type: ${chainId}`); } @@ -2102,7 +2142,10 @@ export function useChainRace() { } ); // Wait for transaction confirmation - await provider.waitForTransaction(transferTxHash); + await provider.waitForTransaction(transferTxHash,{ + retryInterval: 400, + successStates: ['ACCEPTED_ON_L2'] + }); // Calculate transaction latency const endTime = Date.now(); const txLatency = endTime - startTime; @@ -2150,6 +2193,136 @@ export function useChainRace() { } } + setResults(prev => + prev.map(r => + r.chainId === chainId + ? { + ...r, + status: "error" as const, + error: errorMessage + } + : r + ) + ); + break; + } + } + } else if (isPwrChain(chain)) { + // PWR chain transaction processing + if (!pwrWallet) { + console.error(`PWR wallet not ready for ${chainId}`); + setResults(prev => + prev.map(r => + r.chainId === chainId + ? { + ...r, + status: "error" as const, + error: "PWR wallet not ready" + } + : r + ) + ); + return; + } + + // Run the specified number of transactions + for (let txIndex = 0; txIndex < transactionCount; txIndex++) { + try { + // Skip if chain already had an error + const currentState = results.find(r => r.chainId === chainId); + if (currentState?.status === "error") { + break; + } + + let txLatency = 0; + const txStartTime = Date.now(); + + // Send PWR transfer transaction (self-transfer with 0 amount) + const response = await pwrWallet.transferPWR(pwrAddress!, BigInt(0)); + + if (!response.success) { + throw new Error(`PWR transaction failed: ${response.message}`); + } + + // Calculate transaction latency + const txEndTime = Date.now(); + txLatency = txEndTime - txStartTime; + + // Update result with transaction hash + setResults(prev => + prev.map(r => + r.chainId === chainId + ? { ...r, txHash: response.hash as Hex } + : r + ) + ); + + // Transaction confirmed, update completed count and track latencies + setResults((prev) => { + const updatedResults = prev.map(r => { + if (r.chainId === chainId) { + const newLatencies = [...r.txLatencies, txLatency]; + const txCompleted = r.txCompleted + 1; + const allTxCompleted = txCompleted >= transactionCount; + + const totalLatency = newLatencies.length > 0 + ? newLatencies.reduce((sum, val) => sum + val, 0) + : undefined; + + const averageLatency = totalLatency !== undefined + ? Math.round(totalLatency / newLatencies.length) + : undefined; + + const newStatus: "pending" | "racing" | "success" | "error" = + allTxCompleted ? "success" : "racing"; + + return { + ...r, + txCompleted, + status: newStatus, + txLatencies: newLatencies, + averageLatency, + totalLatency + }; + } + return r; + }); + + // Only determine rankings when chains finish all transactions + const finishedResults = updatedResults + .filter(r => r.status === "success") + .sort((a, b) => (a.averageLatency || Infinity) - (b.averageLatency || Infinity)); + + // Assign positions to finished results + finishedResults.forEach((result, idx) => { + const position = idx + 1; + updatedResults.forEach((r, i) => { + if (r.chainId === result.chainId) { + updatedResults[i] = { ...r, position }; + } + }); + }); + + return updatedResults; + }); + } catch (error) { + console.error(`PWR race error for chain ${chainId}, tx #${txIndex}:`, error); + + let errorMessage = "PWR transaction failed"; + + if (error instanceof Error) { + const fullMessage = error.message; + + if (fullMessage.includes("insufficient funds")) { + errorMessage = "Insufficient PWR for transaction fees."; + } else if (fullMessage.includes("timeout")) { + errorMessage = "PWR network timeout. Please try again."; + } else { + const firstLine = fullMessage.split('\n')[0]; + errorMessage = firstLine || fullMessage; + } + } + setResults(prev => prev.map(r => r.chainId === chainId diff --git a/packages/app/src/hooks/usePwrEmbeddedWallet.ts b/packages/app/src/hooks/usePwrEmbeddedWallet.ts new file mode 100644 index 0000000..617925a --- /dev/null +++ b/packages/app/src/hooks/usePwrEmbeddedWallet.ts @@ -0,0 +1,62 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Wallet from '@pwrjs/core/wallet'; + +export function usePwrEmbeddedWallet() { + const [wallet, setWallet] = useState(null); + const [address, setAddress] = useState(""); + const [seedPhrase, setSeedPhrase] = useState(""); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + const initializeWallet = () => { + try { + // Check if we have a saved seed phrase in localStorage + const savedSeedPhrase = localStorage.getItem('pwr-seed-phrase'); + + let pwrSeedPhrase: string; + if (savedSeedPhrase) { + pwrSeedPhrase = savedSeedPhrase; + } else { + // Generate a new seed phrase (you might want to use a proper BIP39 library) + // For now, using the example seed phrase from your code + const newWallet = Wallet.newRandom(12); + pwrSeedPhrase = newWallet.getSeedPhrase() || ""; + localStorage.setItem('pwr-seed-phrase', pwrSeedPhrase); + } + + const pwrWallet = Wallet.fromSeedPhrase(pwrSeedPhrase); + const walletAddress = pwrWallet.getAddress(); + + setWallet(pwrWallet); + setAddress(walletAddress); + setSeedPhrase(pwrSeedPhrase); + setIsReady(true); + + console.log('PWR Wallet initialized:', walletAddress); + } catch (error) { + console.error('Failed to initialize PWR wallet:', error); + setIsReady(false); + } + }; + + initializeWallet(); + }, []); + + const resetWallet = () => { + localStorage.removeItem('pwr-seed-phrase'); + setWallet(null); + setAddress(""); + setSeedPhrase(""); + setIsReady(false); + }; + + return { + wallet, + address, + seedPhrase, + isReady, + resetWallet, + }; +} \ No newline at end of file diff --git a/packages/app/src/hooks/useStarknetEmbeddedWallet.ts b/packages/app/src/hooks/useStarknetEmbeddedWallet.ts index b988a8d..57bcf9a 100644 --- a/packages/app/src/hooks/useStarknetEmbeddedWallet.ts +++ b/packages/app/src/hooks/useStarknetEmbeddedWallet.ts @@ -107,7 +107,10 @@ import { useEffect, useState } from "react"; const { transaction_hash: transferTxHash } = await account0.execute(transferCall, { version: 3 }); - await provider.waitForTransaction(transferTxHash); + await provider.waitForTransaction(transferTxHash,{ + retryInterval: 400, + successStates: ['ACCEPTED_ON_L2'] + }); updateProgress("Crunching the numbers...", 40); const balanceofnewaccountTransfer = @@ -132,7 +135,10 @@ import { useEffect, useState } from "react"; updateProgress("Deploying account...", 80); const { transaction_hash: AXdAth, contract_address: AXcontractFinalAddress } = await accountAX.deployAccount(deployAccountPayload, { version: 3 }); - await provider.waitForTransaction(AXdAth); + await provider.waitForTransaction(AXdAth,{ + retryInterval: 400, + successStates: ['ACCEPTED_ON_L2'] + }); updateProgress("Finalizing setup...", 90); const newAccount = new Account(provider, AXcontractFinalAddress, privateKeyAX); diff --git a/packages/app/src/lib/chainColors.ts b/packages/app/src/lib/chainColors.ts index 8a96678..73661f7 100644 --- a/packages/app/src/lib/chainColors.ts +++ b/packages/app/src/lib/chainColors.ts @@ -13,6 +13,7 @@ const CHAIN_COLOR_MAP: Record = { 'solana-testnet': 'chain-solana', // Solana Testnet 'solana-devnet': 'chain-solana', // Solana Devnet 'solana-mainnet': 'chain-solana', // Solana Mainnet + 'pwr-testnet': 'chain-pwr', // PWR Testnet }; /** diff --git a/packages/app/src/pwr/config.ts b/packages/app/src/pwr/config.ts new file mode 100644 index 0000000..9d7875d --- /dev/null +++ b/packages/app/src/pwr/config.ts @@ -0,0 +1,35 @@ +export interface PwrChainConfig { + id: string; + name: string; + rpcUrl: string; + color: string; + logo: string; + testnet: boolean; + layer: 'L1' | 'L2'; + faucetUrl?: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; +} + +export const pwrTestnet: PwrChainConfig = { + id: 'pwr-testnet', + name: 'PWR Testnet', + rpcUrl: 'https://pwrrpc.pwrlabs.io', + color: '#000000', + logo: '/logos/pwrlabs.avif', + testnet: true, + layer: 'L1', + faucetUrl: 'https://faucet.pwrlabs.io', + nativeCurrency: { + name: 'PWR', + symbol: 'PWR', + decimals: 18, + }, +}; + +export const pwrChains: PwrChainConfig[] = [ + pwrTestnet, +]; \ No newline at end of file