From 4890329f990ce37701fef3455b90123b5ddf0254 Mon Sep 17 00:00:00 2001 From: Bot Date: Mon, 31 Aug 2026 01:41:57 +0800 Subject: [PATCH] feat(vault): add user vault factory, non-custodial deposit/withdraw and session key UI --- apps/web/src/context/Web3Context.tsx | 72 ++++- .../src/features/portfolio/PortfolioView.tsx | 138 +++++++- packages/contracts/src/abis/WTFUserVault.json | 294 ++++++++++++++++++ .../contracts/src/abis/WTFVaultFactory.json | 103 ++++++ packages/contracts/src/index.ts | 6 +- packages/contracts/src/robinhoodTestnet.json | 3 +- 6 files changed, 607 insertions(+), 9 deletions(-) create mode 100644 packages/contracts/src/abis/WTFUserVault.json create mode 100644 packages/contracts/src/abis/WTFVaultFactory.json diff --git a/apps/web/src/context/Web3Context.tsx b/apps/web/src/context/Web3Context.tsx index e829a3f..29c147d 100644 --- a/apps/web/src/context/Web3Context.tsx +++ b/apps/web/src/context/Web3Context.tsx @@ -9,9 +9,14 @@ interface Web3ContextType { chainId: number | null; isCorrectNetwork: boolean; wusdBalance: string; + vaultAddress: string | null; + isSessionActive: boolean; connectWallet: () => Promise; switchNetwork: () => Promise; refreshBalance: () => Promise; + depositToVault: (amount: string) => Promise; + withdrawFromVault: (amount: string) => Promise; + enableSessionKey: () => Promise; getMarketContract: (address: string) => ethers.Contract | null; getCollateralContract: () => ethers.Contract | null; } @@ -23,9 +28,14 @@ const Web3Context = createContext({ chainId: null, isCorrectNetwork: false, wusdBalance: '0', + vaultAddress: null, + isSessionActive: false, connectWallet: async () => {}, switchNetwork: async () => {}, refreshBalance: async () => {}, + depositToVault: async () => '', + withdrawFromVault: async () => '', + enableSessionKey: async () => {}, getMarketContract: () => null, getCollateralContract: () => null, }); @@ -36,9 +46,23 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children const [signer, setSigner] = useState(null); const [chainId, setChainId] = useState(null); const [wusdBalance, setWusdBalance] = useState('0'); + const [vaultAddress, setVaultAddress] = useState(null); + const [isSessionActive, setIsSessionActive] = useState(false); const isCorrectNetwork = chainId === ROBINHOOD_TESTNET_CHAIN.id; + const predictUserVault = async (userAddr: string, prov: ethers.BrowserProvider) => { + try { + const factoryAddr = DEPLOYMENTS.robinhoodTestnet.contracts.vaultFactory; + if (!factoryAddr) return; + const factoryContract = new ethers.Contract(factoryAddr, ABIS.WTFVaultFactory, prov); + const predicted = await factoryContract.predictVaultAddress(userAddr); + setVaultAddress(predicted); + } catch (e) { + console.error('Failed to predict vault address:', e); + } + }; + const refreshBalance = async () => { if (!account || !provider) return; try { @@ -73,10 +97,13 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children (window as any).ethereum.on('accountsChanged', async (accs: string[]) => { if (accs.length > 0) { setAccount(accs[0]); - setSigner(await browserProvider.getSigner()); + const currentSigner = await browserProvider.getSigner(); + setSigner(currentSigner); + predictUserVault(accs[0], browserProvider); } else { setAccount(null); setSigner(null); + setVaultAddress(null); } }); @@ -157,6 +184,44 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children ); }; + const depositToVault = async (amountStr: string): Promise => { + if (!signer || !account || !vaultAddress) throw new Error('Wallet/Vault not ready'); + const collateral = new ethers.Contract( + DEPLOYMENTS.robinhoodTestnet.contracts.collateral, + ABIS.MockERC20, + signer + ); + const amountWei = ethers.parseUnits(amountStr, 18); + const tx = await collateral.transfer(vaultAddress, amountWei); + await tx.wait(); + refreshBalance(); + return tx.hash; + }; + + const withdrawFromVault = async (amountStr: string): Promise => { + if (!signer || !account || !vaultAddress) throw new Error('Wallet/Vault not ready'); + const vault = new ethers.Contract(vaultAddress, ABIS.WTFUserVault, signer); + const amountWei = ethers.parseUnits(amountStr, 18); + const tx = await vault.withdraw( + DEPLOYMENTS.robinhoodTestnet.contracts.collateral, + account, + amountWei + ); + await tx.wait(); + refreshBalance(); + return tx.hash; + }; + + const enableSessionKey = async () => { + if (!signer || !account || !vaultAddress) throw new Error('Wallet/Vault not ready'); + const vault = new ethers.Contract(vaultAddress, ABIS.WTFUserVault, signer); + const relayerAdmin = DEPLOYMENTS.robinhoodTestnet.governance.admin; + const duration = 24 * 3600; // 24 hours + const tx = await vault.authorizeSession(relayerAdmin, duration); + await tx.wait(); + setIsSessionActive(true); + }; + return ( = ({ children chainId, isCorrectNetwork, wusdBalance, + vaultAddress, + isSessionActive, connectWallet, switchNetwork, refreshBalance, + depositToVault, + withdrawFromVault, + enableSessionKey, getMarketContract, getCollateralContract, }} diff --git a/apps/web/src/features/portfolio/PortfolioView.tsx b/apps/web/src/features/portfolio/PortfolioView.tsx index 9a5d457..76831a9 100644 --- a/apps/web/src/features/portfolio/PortfolioView.tsx +++ b/apps/web/src/features/portfolio/PortfolioView.tsx @@ -1,9 +1,51 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useWeb3 } from '../../context/Web3Context'; -import { PieChart, Clock, ArrowUpRight, ArrowDownLeft, ShieldCheck, History } from 'lucide-react'; +import { PieChart, Clock, ArrowUpRight, ArrowDownLeft, ShieldCheck, History, KeyRound, ArrowRightLeft, Loader2, Check } from 'lucide-react'; export const PortfolioView: React.FC = () => { - const { account, wusdBalance } = useWeb3(); + const { account, wusdBalance, vaultAddress, isSessionActive, depositToVault, withdrawFromVault, enableSessionKey } = useWeb3(); + const [depositAmount, setDepositAmount] = useState('500'); + const [withdrawAmount, setWithdrawAmount] = useState('100'); + const [loading, setLoading] = useState(false); + const [actionMsg, setActionMsg] = useState(null); + + const handleDeposit = async () => { + try { + setLoading(true); + setActionMsg(null); + const hash = await depositToVault(depositAmount); + setActionMsg(`🎉 成功存入 ${depositAmount} WUSD 到专属非托管金库!Tx: ${hash.slice(0, 10)}...`); + } catch (e: any) { + alert(e.message || '充值失败'); + } finally { + setLoading(false); + } + }; + + const handleWithdraw = async () => { + try { + setLoading(true); + setActionMsg(null); + const hash = await withdrawFromVault(withdrawAmount); + setActionMsg(`🎉 成功从金库提现 ${withdrawAmount} WUSD 到钱包!Tx: ${hash.slice(0, 10)}...`); + } catch (e: any) { + alert(e.message || '提现失败'); + } finally { + setLoading(false); + } + }; + + const handleEnableSession = async () => { + try { + setLoading(true); + await enableSessionKey(); + setActionMsg('⚡ Session Key 免密连击授权成功!24小时内高频交易零弹窗!'); + } catch (e: any) { + alert(e.message || '授权失败'); + } finally { + setLoading(false); + } + }; const MOCK_POSITIONS = [ { @@ -79,12 +121,96 @@ export const PortfolioView: React.FC = () => {
- 活跃预测头寸 (Active Positions) - 2 个市场 -
持仓均基于不可变智能合约保护
+ 专属非托管智能金库 (AA User Vault) + + {vaultAddress || '连接钱包后自动生成'} + +
+ + 100% 资金链上非托管 +
+ {/* Non-Custodial Vault Operations Panel */} +
+
+
+
+ +
+
+

非托管智能金库与免密连击管理 (AA Smart Vault)

+

平台无法转移你的资产,充值后可开启 Session Key 实现毫秒级 0 弹窗高频交易

+
+
+ + +
+ +
+ {/* Deposit */} +
+ 充值到专属智能金库 (Deposit to Vault) +
+ setDepositAmount(e.target.value)} + placeholder="500" + className="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-100 focus:outline-none focus:border-cyan-500" + /> + +
+
+ + {/* Withdraw */} +
+ 随时提现到 EOA 钱包 (Instant Withdraw) +
+ setWithdrawAmount(e.target.value)} + placeholder="100" + className="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-100 focus:outline-none focus:border-cyan-500" + /> + +
+
+
+ + {actionMsg && ( +
+ + {actionMsg} +
+ )} +
+ {/* Positions Table */}
diff --git a/packages/contracts/src/abis/WTFUserVault.json b/packages/contracts/src/abis/WTFUserVault.json new file mode 100644 index 0000000..a661993 --- /dev/null +++ b/packages/contracts/src/abis/WTFUserVault.json @@ -0,0 +1,294 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "Executed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sessionKey", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + } + ], + "name": "SessionAuthorized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sessionKey", + "type": "address" + } + ], + "name": "SessionRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approveToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sessionKey", + "type": "address" + }, + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + } + ], + "name": "authorizeSession", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "execute", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isRelayer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sessionKey", + "type": "address" + } + ], + "name": "revokeSession", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "sessionExpiry", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/packages/contracts/src/abis/WTFVaultFactory.json b/packages/contracts/src/abis/WTFVaultFactory.json new file mode 100644 index 0000000..5338f6e --- /dev/null +++ b/packages/contracts/src/abis/WTFVaultFactory.json @@ -0,0 +1,103 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "vault", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "vaultIndex", + "type": "uint256" + } + ], + "name": "VaultCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allVaults", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "getOrCreateVault", + "outputs": [ + { + "internalType": "address", + "name": "vault", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "getVault", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "predictVaultAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 06a2a57..4e7740d 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1,12 +1,16 @@ import WTFControllerV2Abi from './abis/WTFControllerV2.json'; import MockERC20Abi from './abis/MockERC20.json'; import WTFMarketV2Abi from './abis/WTFMarketV2.json'; +import WTFVaultFactoryAbi from './abis/WTFVaultFactory.json'; +import WTFUserVaultAbi from './abis/WTFUserVault.json'; import robinhoodTestnetDeployment from './robinhoodTestnet.json'; export const ABIS = { WTFControllerV2: WTFControllerV2Abi, MockERC20: MockERC20Abi, - WTFMarketV2: WTFMarketV2Abi + WTFMarketV2: WTFMarketV2Abi, + WTFVaultFactory: WTFVaultFactoryAbi, + WTFUserVault: WTFUserVaultAbi }; export const DEPLOYMENTS = { diff --git a/packages/contracts/src/robinhoodTestnet.json b/packages/contracts/src/robinhoodTestnet.json index 6caa952..f576df4 100644 --- a/packages/contracts/src/robinhoodTestnet.json +++ b/packages/contracts/src/robinhoodTestnet.json @@ -7,7 +7,8 @@ "collateral": "0xe776e957953EA69b7Eaa9d7d4098aBC076bDD5E7", "powerLDACurve": "0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1", "controllerImplementation": "0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75", - "controllerProxy": "0xc0E24E152771C588B21AEB654b30B1cBAf381c1a" + "controllerProxy": "0xc0E24E152771C588B21AEB654b30B1cBAf381c1a", + "vaultFactory": "0x89401e07296267c01017cA150D5Ec8883a78e0B2" }, "governance": { "admin": "0x6cddF384792C77219fc25C454Cfe264757842830",