UNPKG

@arbius/aa-wallet

Version:

A secure and flexible Account Abstraction wallet implementation for Arbitrum One chain applications.

444 lines (425 loc) 21.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = require("react"); const TransactionToast_1 = require("./components/TransactionToast"); const lucide_react_1 = require("lucide-react"); const qrcode_react_1 = require("qrcode.react"); const ethers_1 = require("ethers"); const arbius_svg_1 = __importDefault(require("./assets/arbius.svg")); const deterministicWalletUtils_1 = require("./utils/deterministicWalletUtils"); // Arbitrum One Chain ID const ARBITRUM_CHAIN_ID = 42161; // AIUS Token Contract const AIUS_TOKEN_ADDRESS = '0x4a24B101728e07A52053c13FB4dB2BcF490CAbc3'; // Minimal ERC20 ABI for balance checking const ERC20_ABI = [ { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "type": "function" } ]; function App() { // EOA (External Owned Account) state const [isConnected, setIsConnected] = (0, react_1.useState)(false); const [eoaAddress, setEoaAddress] = (0, react_1.useState)(null); const [ethersSigner, setEthersSigner] = (0, react_1.useState)(null); const [ethersProvider, setEthersProvider] = (0, react_1.useState)(null); // Deterministic wallet state const [deterministicWalletInstance, setDeterministicWalletInstance] = (0, react_1.useState)(null); const [deterministicWalletAddress, setDeterministicWalletAddress] = (0, react_1.useState)(null); // UI state const [amount, setAmount] = (0, react_1.useState)('0.01'); const [isLoading, setIsLoading] = (0, react_1.useState)(false); const [showDepositModal, setShowDepositModal] = (0, react_1.useState)(false); const [showWithdrawModal, setShowWithdrawModal] = (0, react_1.useState)(false); const [balance, setBalance] = (0, react_1.useState)('0'); const [aiusBalance, setAiusBalance] = (0, react_1.useState)('0'); const [sendMax, setSendMax] = (0, react_1.useState)(false); const [isCorrectNetwork, setIsCorrectNetwork] = (0, react_1.useState)(false); const [selectedToken, setSelectedToken] = (0, react_1.useState)('ETH'); // Connect to EOA (MetaMask) const connectWallet = async () => { if (!window.ethereum) { alert('Please install MetaMask!'); return; } try { const provider = new ethers_1.ethers.BrowserProvider(window.ethereum); setEthersProvider(provider); const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const address = accounts[0]; setEoaAddress(address); const signer = await provider.getSigner(); setEthersSigner(signer); setIsConnected(true); // Initialize deterministic wallet after EOA connection await initializeDeterministicWallet(address, signer, provider); } catch (error) { console.error('Failed to connect wallet:', error); } }; // Initialize deterministic wallet const initializeDeterministicWallet = async (ownerAddress, signer, provider) => { try { const wallet = await (0, deterministicWalletUtils_1.initDeterministicWallet)(ethers_1.ethers, ownerAddress, async (message) => signer.signMessage(message), provider); setDeterministicWalletInstance(wallet); setDeterministicWalletAddress(wallet.address); console.log('Deterministic wallet initialized:', wallet.address); } catch (error) { console.error('Failed to initialize deterministic wallet:', error); } }; // Check current network (0, react_1.useEffect)(() => { const checkNetwork = async () => { if (!window.ethereum) return; try { const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' }); const chainId = parseInt(chainIdHex, 16); setIsCorrectNetwork(chainId === ARBITRUM_CHAIN_ID); } catch (error) { console.error('Failed to get chain ID:', error); } }; checkNetwork(); if (window.ethereum) { window.ethereum.on('chainChanged', (chainIdHex) => { const chainId = parseInt(chainIdHex, 16); setIsCorrectNetwork(chainId === ARBITRUM_CHAIN_ID); }); } return () => { if (window.ethereum && window.ethereum.removeListener) { window.ethereum.removeListener('chainChanged', () => { }); } }; }, []); // Switch to Arbitrum network const switchToArbitrum = async () => { if (!window.ethereum) return; try { try { await window.ethereum.request({ method: 'wallet_addEthereumChain', params: [{ chainId: `0x${ARBITRUM_CHAIN_ID.toString(16)}`, chainName: 'Arbitrum One', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: ['https://arb1.arbitrum.io/rpc'], blockExplorerUrls: ['https://arbiscan.io/'] }] }); } catch (addError) { console.log('Network may already exist', addError); } await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: `0x${ARBITRUM_CHAIN_ID.toString(16)}` }] }); } catch (error) { console.error('Failed to switch network:', error); } }; // Fetch ETH balance (0, react_1.useEffect)(() => { const checkBalance = async () => { if (!isConnected || !deterministicWalletAddress || !window.ethereum || !isCorrectNetwork) return; try { const balance = await window.ethereum.request({ method: 'eth_getBalance', params: [deterministicWalletAddress, 'latest'], }); setBalance((0, ethers_1.formatEther)(balance)); } catch (error) { console.error('Failed to fetch ETH balance:', error); } }; checkBalance(); const interval = setInterval(checkBalance, 10000); return () => clearInterval(interval); }, [isConnected, deterministicWalletAddress, isCorrectNetwork]); // Fetch AIUS token balance (0, react_1.useEffect)(() => { const fetchTokenBalance = async () => { if (!isConnected || !deterministicWalletAddress || !window.ethereum || !isCorrectNetwork) return; try { const provider = new ethers_1.ethers.BrowserProvider(window.ethereum); const tokenContract = new ethers_1.Contract(AIUS_TOKEN_ADDRESS, ERC20_ABI, provider); const rawBalance = await tokenContract.balanceOf(deterministicWalletAddress); const formattedBalance = (0, ethers_1.formatEther)(rawBalance); setAiusBalance(formattedBalance); console.log(`AIUS Balance: ${formattedBalance}`); } catch (error) { console.error('Failed to fetch AIUS balance:', error); setAiusBalance('0'); } }; fetchTokenBalance(); const interval = setInterval(fetchTokenBalance, 10000); return () => clearInterval(interval); }, [isConnected, deterministicWalletAddress, isCorrectNetwork]); const handleDeposit = () => { if (!isConnected || !deterministicWalletAddress || !isCorrectNetwork) return; setShowDepositModal(true); }; const handleWithdraw = async () => { if (!isConnected || !deterministicWalletAddress || !eoaAddress || !isCorrectNetwork) return; setShowWithdrawModal(true); }; const executeWithdraw = async () => { if (!deterministicWalletInstance || !eoaAddress) return; setIsLoading(true); try { const txHash = await (0, deterministicWalletUtils_1.withdrawFromDeterministicWallet)(ethers_1.ethers, deterministicWalletInstance, eoaAddress, { amount: sendMax ? undefined : amount, token: selectedToken }); if (txHash) { console.log(`${selectedToken} Withdrawal successful:`, txHash); } else { console.error(`${selectedToken} Withdrawal failed: No transaction hash returned`); } } catch (error) { console.error('Withdrawal failed:', error); } finally { setIsLoading(false); setShowWithdrawModal(false); } }; const DepositModal = () => { if (!showDepositModal || !deterministicWalletAddress) return null; // Generate ERC-681 compliant URL const generateERC681Url = () => { const baseUrl = `ethereum:${deterministicWalletAddress}`; const params = new URLSearchParams(); params.append('chainId', ARBITRUM_CHAIN_ID.toString()); if (selectedToken === 'ETH') { params.append('value', (0, ethers_1.parseEther)(amount).toString()); } else { params.append('function', 'transfer'); params.append('uint256', (0, ethers_1.parseEther)(amount).toString()); params.append('token', AIUS_TOKEN_ADDRESS); } return `${baseUrl}?${params.toString()}`; }; const depositLink = generateERC681Url(); return (<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="bg-white rounded-xl shadow-xl p-6 max-w-md w-full"> <div className="flex justify-between items-center mb-4"> <h2 className="text-xl font-bold">Deposit {selectedToken} on Arbitrum</h2> <button onClick={() => setShowDepositModal(false)} className="text-gray-500 hover:text-gray-700"> <lucide_react_1.X className="w-6 h-6"/> </button> </div> <div className="space-y-4"> <div className="bg-gray-50 p-4 rounded-lg"> <p className="text-sm text-gray-600 mb-2">Deposit to your AA Wallet</p> <p className="text-2xl font-bold text-indigo-600">{selectedToken} (Arbitrum One)</p> <p className="text-lg font-semibold text-gray-700 mt-2"> Amount: {parseFloat(amount).toFixed(4)} {selectedToken} </p> </div> <div className="bg-gray-50 p-4 rounded-lg"> <p className="text-sm text-gray-600 mb-2">AA Wallet Address</p> <a href={`https://arbiscan.io/address/${deterministicWalletAddress}`} target="_blank" rel="noopener noreferrer" className="font-mono text-sm break-all text-blue-600 hover:text-blue-800 hover:underline"> {deterministicWalletAddress} </a> </div> <div className="flex justify-center p-4 bg-white"> <qrcode_react_1.QRCodeSVG value={depositLink} size={200} level="H" includeMargin={true}/> </div> <p className="text-sm text-gray-500 text-center"> Scan this QR code with your wallet app or copy the address above </p> <div className="bg-blue-100 p-4 rounded-lg"> <p className="text-sm text-blue-800"> <strong>Important:</strong> Make sure you are on the Arbitrum One network when sending funds. </p> </div> </div> </div> </div>); }; const WithdrawConfirmationModal = () => { if (!showWithdrawModal || !eoaAddress) return null; const maxBalance = selectedToken === 'ETH' ? balance : aiusBalance; const displayAmount = sendMax ? parseFloat(maxBalance).toFixed(4) : parseFloat(amount).toFixed(4); return (<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="bg-white rounded-xl shadow-xl p-6 max-w-md w-full"> <div className="flex justify-between items-center mb-4"> <h2 className="text-xl font-bold">Confirm Withdrawal</h2> <button onClick={() => setShowWithdrawModal(false)} className="text-gray-500 hover:text-gray-700"> <lucide_react_1.X className="w-6 h-6"/> </button> </div> <div className="space-y-4"> <div className="bg-gray-50 p-4 rounded-lg"> <p className="text-sm text-gray-600 mb-2">Withdrawal Amount</p> <p className="text-2xl font-bold text-indigo-600"> {displayAmount} {selectedToken} </p> </div> <div className="bg-gray-50 p-4 rounded-lg"> <p className="text-sm text-gray-600 mb-2">Recipient Address</p> <p className="font-mono text-sm break-all">{eoaAddress}</p> </div> <div className="bg-blue-100 p-4 rounded-lg"> <p className="text-sm text-blue-800"> <strong>Note:</strong> This will withdraw {selectedToken} from your AA Wallet back to your connected wallet. </p> </div> <div className="grid grid-cols-2 gap-4 mt-4"> <button onClick={() => setShowWithdrawModal(false)} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> Cancel </button> <button onClick={executeWithdraw} disabled={isLoading} className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#0A0047] hover:bg-[#07003a] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0A0047] disabled:opacity-50"> {isLoading ? (<lucide_react_1.Loader className="w-4 h-4 animate-spin mx-auto"/>) : ('Confirm Withdrawal')} </button> </div> </div> </div> </div>); }; return (<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 flex flex-col items-center justify-center p-4"> <div className="bg-white rounded-xl shadow-xl p-8 max-w-md w-full space-y-6"> <div className="flex items-center justify-between"> <div className="flex items-center space-x-2"> <img src={arbius_svg_1.default} alt="Arbius Logo" className="w-7 h-7"/> <span className="text-2xl font-bold text-black ml-1">Arbius</span> <span className="mx-3 h-6 border-l border-gray-300"/> <span className="text-2xl font-light uppercase" style={{ color: '#0A0047', letterSpacing: '0.05em' }}>WALLET</span> </div> </div> <div className="w-full mt-2"> <button onClick={connectWallet} disabled={isConnected} className={`w-full px-8 py-2 text-lg text-center justify-center rounded-md shadow-sm font-medium text-white bg-[#0A0047] hover:bg-[#07003a] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0A0047] disabled:opacity-50`}> {isConnected ? 'Connected' : 'Connect Wallet'} </button> </div> {!isCorrectNetwork && isConnected && (<div className="bg-yellow-100 p-4 rounded-lg"> <p className="text-sm text-yellow-800 mb-2"> Please switch to Arbitrum One network to use this wallet. </p> <button onClick={switchToArbitrum} className="w-full text-sm bg-yellow-500 hover:bg-yellow-600 text-white py-2 px-4 rounded transition"> Switch to Arbitrum </button> </div>)} {isConnected && (<> <div className="space-y-4"> <div className="bg-gray-50 rounded-lg p-4"> <div className="flex justify-between items-center"> <p className="text-sm text-gray-600">Network</p> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> Arbitrum One </span> </div> </div> <div className="bg-gray-50 rounded-lg p-4"> <p className="text-sm text-gray-600">AA Wallet Address</p> <a href={`https://arbiscan.io/address/${deterministicWalletAddress}`} target="_blank" rel="noopener noreferrer" className="font-mono text-sm break-all text-blue-600 hover:text-blue-800 hover:underline"> {deterministicWalletAddress} </a> </div> <div className="bg-gray-50 rounded-lg p-4"> <div className="flex flex-col space-y-2"> <div> <p className="text-sm text-gray-600">ETH Balance</p> <p className="text-xl font-bold text-indigo-600">{parseFloat(balance).toFixed(4)} ETH</p> </div> <div className="pt-2 border-t border-gray-200"> <p className="text-sm text-gray-600">AIUS Balance</p> <p className="text-xl font-bold text-purple-600">{parseFloat(aiusBalance).toFixed(4)} AIUS</p> </div> </div> </div> <div className="space-y-2"> <div className="flex justify-between items-center"> <label className="block text-sm font-medium text-gray-700"> Amount </label> <select value={selectedToken} onChange={(e) => setSelectedToken(e.target.value)} className="block w-32 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"> <option value="ETH">ETH</option> <option value="AIUS">AIUS</option> </select> </div> <input type="number" value={sendMax ? (selectedToken === 'ETH' ? balance : aiusBalance) : amount} onChange={(e) => setAmount(e.target.value)} min="0" step="0.01" disabled={sendMax || !isCorrectNetwork} className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 disabled:bg-gray-100"/> <div className="flex items-center mt-2"> <input type="checkbox" id="sendMax" checked={sendMax} onChange={() => setSendMax(!sendMax)} disabled={!isCorrectNetwork} className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"/> <label htmlFor="sendMax" className="ml-2 block text-sm text-gray-900"> Send Max (will auto-adjust for gas fees) </label> </div> </div> <div className="grid grid-cols-2 gap-4"> <button onClick={handleDeposit} disabled={isLoading || !isCorrectNetwork} className="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#0A0047] hover:bg-[#07003a] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0A0047] disabled:opacity-50"> {isLoading ? (<lucide_react_1.Loader className="w-4 h-4 animate-spin"/>) : (<> <lucide_react_1.ArrowDownToLine className="w-4 h-4 mr-2"/> Deposit </>)} </button> <button onClick={handleWithdraw} disabled={isLoading || !isCorrectNetwork} className="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#0A0047] hover:bg-[#07003a] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0A0047] disabled:opacity-50"> {isLoading ? (<lucide_react_1.Loader className="w-4 h-4 animate-spin"/>) : (<> <lucide_react_1.ArrowUpFromLine className="w-4 h-4 mr-2"/> Withdraw </>)} </button> </div> </div> </>)} </div> <DepositModal /> <WithdrawConfirmationModal /> <TransactionToast_1.TransactionToast position="bottom-right"/> <div className="mt-4 text-xs text-gray-500"> <a href="https://arbiscan.io/" target="_blank" rel="noopener noreferrer" className="underline">Arbiscan</a> {' • '} <a href="https://bridge.arbitrum.io/" target="_blank" rel="noopener noreferrer" className="underline">Arbitrum Bridge</a> {' • '} <a href={`https://arbiscan.io/token/${AIUS_TOKEN_ADDRESS}`} target="_blank" rel="noopener noreferrer" className="underline">AIUS Token</a> </div> </div>); } exports.default = App;