diff --git a/web_wallet/src/screens/Wallet.tsx b/web_wallet/src/screens/Wallet.tsx index 92e749e..3b389e2 100644 --- a/web_wallet/src/screens/Wallet.tsx +++ b/web_wallet/src/screens/Wallet.tsx @@ -4,105 +4,156 @@ import { VMABI } from 'hypersdk-client/src/lib/Marshaler'; import { ArrowPathIcon } from '@heroicons/react/20/solid' import { stringify } from 'lossless-json' +const getDefaultValue = (fieldType: string) => { + if (fieldType === 'Address') return "00" + "00".repeat(27) + "00deadc0de" + if (fieldType === 'Bytes') return btoa('Luigi'); + if (fieldType === 'string') return 'Hello'; + if (fieldType === 'uint64') return '123456789'; + if (fieldType.startsWith('int') || fieldType.startsWith('uint')) return '0'; + return ''; +} -export default function Wallet({ myAddr }: { myAddr: string }) { - const [balance, setBalance] = useState(0n) - const [loading, setLoading] = useState(0) - const [error, setError] = useState(null) - const [abi, setAbi] = useState(null) - const [actionLogs, setActionLogs] = useState>({}) - const [actionInputs, setActionInputs] = useState>>({}) - //balance stuff - const fetchBalance = useCallback(async () => { - setLoading(l => l + 1) - try { - const balance = await vmClient.getBalance(myAddr) - setBalance(balance) - setError(null) - } catch (e) { - console.error("Failed to fetch balance:", e) - setError((e instanceof Error && e.message) ? e.message : String(e)) - } finally { - setLoading(l => l - 1) - } - }, [myAddr]); +function Action({ actionName, abi, fetchBalance }: { actionName: string, abi: VMABI, fetchBalance: (waitForChange: boolean) => void }) { + const actionType = abi.types.find(t => t.name === actionName) + const action = abi.actions.find(a => a.name === actionName) + const [actionLogs, setActionLogs] = useState([]) + const [actionInputs, setActionInputs] = useState>({}) useEffect(() => { - fetchBalance() - }, [fetchBalance]) - - useEffect(() => { - setLoading(l => l + 1) - vmClient.getAbi() - .then(setAbi) - .catch(e => { - console.error("Failed to fetch ABI:", e) - setError((e instanceof Error && e.message) ? e.message : String(e)) + if (actionType) { + setActionInputs(prev => { + for (const field of actionType.fields) { + if (!(field.name in prev)) { + prev[field.name] = getDefaultValue(field.type) + } + } + return prev }) - .finally(() => setLoading(l => l - 1)) - }, []) + } + }, [actionName, actionType]); const executeAction = async (actionName: string, isReadOnly: boolean) => { + setActionLogs([]) const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - setActionLogs(prev => ({ ...prev, [actionName]: `${now} - Executing...` })) + setActionLogs(prev => [...prev, `${now} - Executing...`]) try { - const data = getFilledStructData(actionName) - setActionLogs(prev => ({ ...prev, [actionName]: `Action data for ${actionName}: ${JSON.stringify(data, null, 2)}` })) + setActionLogs(prev => [...prev, `Action data for ${actionName}: ${JSON.stringify(actionInputs, null, 2)}`]) const result = isReadOnly - ? await vmClient.executeReadonlyAction({ actionName, data }) - : await vmClient.sendTx([{ actionName, data }]) + ? await vmClient.executeReadonlyAction({ actionName, data: actionInputs }) + : await vmClient.sendTx([{ actionName, data: actionInputs }]) const endTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - setActionLogs(prev => ({ ...prev, [actionName]: `${endTime} - Success: ${stringify(result)}` })) - fetchBalance() + setActionLogs(prev => [...prev, `${endTime} - Success: ${stringify(result)}`]) + if (!isReadOnly) { + fetchBalance(true) + } } catch (e) { console.error(e) const errorTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - setActionLogs(prev => ({ ...prev, [actionName]: `${errorTime} - Error: ${e}` })) + setActionLogs(prev => [...prev, `${errorTime} - Error: ${e}`]) } } - function getFilledStructData(typeName: string): Record { - const structData: Record = actionInputs[typeName] || {} - const structType = abi?.types.find(type => type.name === typeName) - if (!structType) { - throw new Error(`Struct ${typeName} not found in ABI`) - } - for (const field of structType.fields) { - if (typeof structData[field.name] === 'undefined') { - structData[field.name] = getDefaultValue(field.type); - } - } - return structData - } - - - const handleInputChange = (actionName: string, fieldName: string, value: string) => { + const handleInputChange = (fieldName: string, value: string) => { setActionInputs(prev => ({ - ...prev, - [actionName]: { - ...(prev[actionName] || {}), - [fieldName]: value - } + ...prev, [fieldName]: value })) } - const getDefaultValue = (fieldType: string) => { - if (fieldType === 'Address') return "00" + "00".repeat(27) + "00deadc0de" - if (fieldType === 'Bytes') return btoa('Luigi'); - if (fieldType === 'string') return 'Hello'; - if (fieldType === 'uint64') return '1234567890'; - if (fieldType.startsWith('int') || fieldType.startsWith('uint')) return '0'; - return ''; + if (!action) { + return
Action not found
} - if (loading > 0) { - return
Loading...
- } + return ( +
+

{action.name}

+
+

Input Fields:

+ {actionType?.fields.map(field => { + if (field.type.includes('[]')) { + return

Warning: Array type not supported for {field.name}

+ } + return ( +
+ + handleInputChange(field.name, e.target.value)} + /> +
+ ) + })} +
+
+ + +
+
+

Log:

+
{actionLogs.join('\n') || 'No logs yet.'}
+
+
+ ) +} - if (error) { - return
Error: {error}
- } +export default function Wallet({ myAddr }: { myAddr: string }) { + const [balance, setBalance] = useState(null) + const [balanceLoading, setBalanceLoading] = useState(false) + const [balanceError, setBalanceError] = useState(null) + const [abi, setAbi] = useState(null) + const [abiLoading, setAbiLoading] = useState(false) + const [abiError, setAbiError] = useState(null) + + // Balance fetching + const fetchBalance = useCallback(async (waitForChange: boolean = false) => { + setBalanceLoading(true) + try { + if (waitForChange) { + await new Promise(resolve => setTimeout(resolve, 3 * 1000)) + //TODO: actually wait for the balance to change + } + let newBalance = await vmClient.getBalance(myAddr) + + setBalance(newBalance) + setBalanceError(null) + } catch (e) { + console.error("Failed to fetch balance:", e) + setBalanceError((e instanceof Error && e.message) ? e.message : String(e)) + } finally { + setBalanceLoading(false) + } + }, [myAddr]); + + useEffect(() => { + fetchBalance() + }, [fetchBalance]) + + // ABI fetching + useEffect(() => { + setAbiLoading(true) + vmClient.getAbi() + .then(newAbi => { + setAbi(newAbi) + setAbiError(null) + }) + .catch(e => { + console.error("Failed to fetch ABI:", e) + setAbiError((e instanceof Error && e.message) ? e.message : String(e)) + }) + .finally(() => setAbiLoading(false)) + }, []) return (
@@ -112,62 +163,31 @@ export default function Wallet({ myAddr }: { myAddr: string }) {

Balance:

-
-
- {parseFloat(vmClient.formatBalance(balance)).toFixed(6)} {vmClient.COIN_SYMBOL} + {balanceLoading ? ( +
Loading balance...
+ ) : balanceError ? ( +
Error loading balance: {balanceError}
+ ) : balance !== null ? ( +
+
+ {parseFloat(vmClient.formatBalance(balance)).toFixed(6)} {vmClient.COIN_SYMBOL} +
+
- -
+ ) : null}
- {abi?.actions.map(action => { - const actionType = abi.types.find(t => t.name === action.name) - return ( -
-

{action.name}

-
-

Input Fields:

- {actionType?.fields.map(field => { - if (field.type.includes('[]')) { - return

Warning: Array type not supported for {field.name}

- } - const defaultValue = getDefaultValue(field.type); - return ( -
- - handleInputChange(action.name, field.name, e.target.value)} - /> -
- ) - })} -
-
- - -
-
-

Log:

-
{actionLogs[action.name] || 'No logs yet.'}
-
-
- ) - })} + {abiLoading ? ( +
Loading ABI...
+ ) : abiError ? ( +
Error loading ABI: {abiError}
+ ) : abi ? ( + abi.actions.map(action => ( + + )) + ) : null}
)