@arbius/aa-wallet
Version:
A secure and flexible Account Abstraction wallet implementation for Arbitrum One chain applications.
301 lines (300 loc) • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useAAWallet = useAAWallet;
const react_1 = require("react");
const AAWalletProvider_1 = require("../components/AAWalletProvider");
const ethers_1 = require("ethers");
// 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"
}
];
// Storage key for the derived wallet
const DERIVED_WALLET_STORAGE_KEY = 'derivedWalletCache';
function useAAWallet() {
const context = (0, react_1.useContext)(AAWalletProvider_1.AAWalletContext);
const [smartAccountAddress, setSmartAccountAddress] = (0, react_1.useState)(null);
const [derivedWallet, setDerivedWallet] = (0, react_1.useState)(null);
const [provider, setProvider] = (0, react_1.useState)(null);
(0, react_1.useEffect)(() => {
const initDerivedWallet = async () => {
if (!context.isConnected || !context.address || !window.ethereum)
return;
try {
// Create ethers provider for Arbitrum
const provider = new ethers_1.ethers.BrowserProvider(window.ethereum);
setProvider(provider);
// Check if we already have a cached wallet for this address
const cachedWalletJson = localStorage.getItem(DERIVED_WALLET_STORAGE_KEY);
let cachedWallet = null;
if (cachedWalletJson) {
try {
const parsed = JSON.parse(cachedWalletJson);
// Ensure the cache is for the current connected address
if (parsed.ownerAddress.toLowerCase() === context.address.toLowerCase()) {
cachedWallet = parsed;
console.log('Found cached derived wallet');
}
}
catch (e) {
console.error('Error parsing cached wallet:', e);
// Clear invalid cache
localStorage.removeItem(DERIVED_WALLET_STORAGE_KEY);
}
}
let walletInstance;
if (cachedWallet) {
// Use the cached wallet
walletInstance = new ethers_1.ethers.Wallet(cachedWallet.derivedPrivateKey);
console.log('Using cached derived wallet:', cachedWallet.derivedAddress);
}
else {
// We need to create a new wallet
console.log('No cached wallet found, creating new one...');
// Get the signer for the connected account
const signer = await provider.getSigner();
// Create a unique message that will be consistent for this address
const message = `Create deterministic wallet for ${context.address.toLowerCase()}`;
// Check if we have a cached signature for this message
const signatureCacheKey = `signature_${context.address.toLowerCase()}`;
let signature = localStorage.getItem(signatureCacheKey);
if (!signature) {
// Get signature from main wallet only if we don't have it cached
signature = await signer.signMessage(message);
// Cache the signature
localStorage.setItem(signatureCacheKey, signature);
}
// Use the signature as entropy to create a new deterministic wallet
walletInstance = new ethers_1.ethers.Wallet(ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(signature)));
// Cache the wallet for future use
const cacheData = {
ownerAddress: context.address,
derivedPrivateKey: walletInstance.privateKey,
derivedAddress: walletInstance.address
};
localStorage.setItem(DERIVED_WALLET_STORAGE_KEY, JSON.stringify(cacheData));
console.log('New derived wallet created and cached');
}
// Connect the wallet to the provider
const connectedWallet = walletInstance.connect(provider);
// Save the derived wallet
setDerivedWallet(connectedWallet);
// Set the wallet address
setSmartAccountAddress(connectedWallet.address);
console.log('Derived wallet ready:', connectedWallet.address);
}
catch (error) {
console.error('Failed to initialize derived wallet:', error);
}
};
// Add a flag to prevent multiple simultaneous initializations
let isInitializing = false;
const init = async () => {
if (isInitializing)
return;
isInitializing = true;
await initDerivedWallet();
isInitializing = false;
};
init();
// Cleanup function
return () => {
isInitializing = true; // Prevent any pending initializations
};
}, [context.isConnected, context.address]);
// Function to send a transaction using the derived wallet
const sendDerivedWalletTransaction = async (tx) => {
if (!derivedWallet || !provider) {
console.error('Derived wallet not initialized');
return null;
}
try {
// Get current wallet balance
const balance = await provider.getBalance(derivedWallet.address);
console.log(`Wallet balance: ${ethers_1.ethers.formatEther(balance)} ETH`);
// Create transaction request for gas estimation
const txRequest = {
to: tx.to,
value: ethers_1.ethers.parseEther(tx.value),
data: tx.data || '0x',
from: derivedWallet.address
};
// Estimate gas using eth_estimateGas
const gasLimit = await provider.estimateGas(txRequest);
console.log(`Estimated gas limit: ${gasLimit}`);
// Get the current gas price from the network
const feeData = await provider.getFeeData();
const gasPrice = feeData.gasPrice || BigInt(100000000); // Fallback to 0.1 Gwei
// Calculate gas cost with 20% buffer for safety
const gasCost = gasPrice * gasLimit * 120n / 100n;
console.log(`Estimated gas cost: ${ethers_1.ethers.formatEther(gasCost)} ETH`);
// Calculate maximum amount that can be sent (balance - gas cost)
let valueToSend = ethers_1.ethers.parseEther(tx.value);
const maxPossible = balance - gasCost;
// If trying to send more than available (including gas), adjust the amount
if (valueToSend >= maxPossible) {
console.log(`Adjusting send amount to account for gas fees`);
if (maxPossible <= 0n) {
throw new Error("Insufficient funds to cover gas costs");
}
// Leave a small amount for gas price fluctuations
valueToSend = maxPossible * 95n / 100n;
console.log(`Adjusted amount: ${ethers_1.ethers.formatEther(valueToSend)} ETH`);
}
// Create final transaction request
const finalTxRequest = {
to: tx.to,
value: valueToSend,
data: tx.data || '0x',
gasLimit: gasLimit,
gasPrice: gasPrice
};
console.log('Sending transaction from derived wallet:', finalTxRequest);
// Sign and send the transaction
const txResponse = await derivedWallet.sendTransaction(finalTxRequest);
// Wait for the transaction to be mined
const receipt = await txResponse.wait();
console.log('Transaction confirmed:', receipt);
return txResponse.hash;
}
catch (error) {
console.error('Failed to send transaction from derived wallet:', error);
return null;
}
};
// NEW: Function to sign a message using the derived AA wallet
const signMessageWithAAWallet = async (message) => {
if (!derivedWallet) {
console.error('Derived AA wallet not initialized or available for signing.');
// Optionally, you could try to re-initialize or prompt for it here if applicable
return null;
}
try {
const signature = await derivedWallet.signMessage(message);
return signature;
}
catch (error) {
console.error('Failed to sign message with AA wallet:', error);
return null;
}
};
// Function to withdraw funds from derived wallet to main wallet
const withdrawToMainWallet = async (options) => {
if (!derivedWallet || !provider || !context.address) {
console.error('Derived wallet or main wallet not initialized');
return null;
}
try {
const token = options.token || 'ETH';
const amount = options.amount;
if (token === 'ETH') {
// Get current wallet balance
const balance = await provider.getBalance(derivedWallet.address);
if (balance === 0n) {
throw new Error('No ETH available to withdraw');
}
// If amount is specified, use it, otherwise withdraw entire balance minus gas
let valueToSend;
if (amount) {
valueToSend = ethers_1.ethers.parseEther(amount);
if (valueToSend >= balance) {
throw new Error('Withdrawal amount exceeds available balance');
}
}
else {
// Estimate gas for the withdrawal transaction
const gasEstimate = await provider.estimateGas({
from: derivedWallet.address,
to: context.address,
value: balance
});
const feeData = await provider.getFeeData();
const gasPrice = feeData.gasPrice || BigInt(100000000);
const gasCost = gasPrice * gasEstimate;
// Leave a small buffer for gas price fluctuations
valueToSend = balance - (gasCost * 120n / 100n);
if (valueToSend <= 0n) {
throw new Error('Insufficient funds to cover gas costs');
}
}
// Create and send the withdrawal transaction
const txResponse = await derivedWallet.sendTransaction({
to: context.address,
value: valueToSend,
gasLimit: await provider.estimateGas({
from: derivedWallet.address,
to: context.address,
value: valueToSend
})
});
// Wait for the transaction to be mined
const receipt = await txResponse.wait();
console.log('ETH withdrawal confirmed:', receipt);
return txResponse.hash;
}
else {
// Handle AIUS token withdrawal
const tokenContract = new ethers_1.ethers.Contract(AIUS_TOKEN_ADDRESS, ERC20_ABI, derivedWallet);
// Get token balance
const balance = await tokenContract.balanceOf(derivedWallet.address);
if (balance === 0n) {
throw new Error('No AIUS tokens available to withdraw');
}
// Get token decimals
const decimals = await tokenContract.decimals();
// If amount is specified, use it, otherwise withdraw entire balance
let valueToSend;
if (amount) {
valueToSend = ethers_1.ethers.parseUnits(amount, decimals);
if (valueToSend >= balance) {
throw new Error('Withdrawal amount exceeds available balance');
}
}
else {
valueToSend = balance;
}
// Create and send the token transfer transaction
const txResponse = await tokenContract.transfer(context.address, valueToSend);
// Wait for the transaction to be mined
const receipt = await txResponse.wait();
console.log('AIUS withdrawal confirmed:', receipt);
return txResponse.hash;
}
}
catch (error) {
console.error('Failed to withdraw from derived wallet:', error);
return null;
}
};
if (!context) {
throw new Error('useAAWallet must be used within an AAWalletProvider');
}
return {
...context,
smartAccountAddress,
sendDerivedWalletTransaction,
signMessageWithAAWallet,
withdrawToMainWallet,
};
}