@arbius/aa-wallet
Version:
A secure and flexible Account Abstraction wallet implementation for Arbitrum One chain applications.
444 lines (425 loc) • 21.9 kB
JavaScript
;
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;