From d1e36a7439dde5c52949a4f7e187c0e24097743a Mon Sep 17 00:00:00 2001 From: samsiegart Date: Fri, 12 Jan 2024 02:49:41 -0800 Subject: [PATCH] feat: auto-provision smart wallet --- src/components/ChainConnection.tsx | 86 +--------- src/components/ProvisionSmartWalletDialog.tsx | 159 +++++++----------- src/components/Swap.tsx | 32 ++-- src/services/wallet.ts | 22 --- src/store/app.ts | 3 - 5 files changed, 84 insertions(+), 218 deletions(-) delete mode 100644 src/services/wallet.ts diff --git a/src/components/ChainConnection.tsx b/src/components/ChainConnection.tsx index 0a250b2..9eb19fe 100644 --- a/src/components/ChainConnection.tsx +++ b/src/components/ChainConnection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { toast } from 'react-toastify'; import { Oval } from 'react-loader-spinner'; @@ -16,7 +16,6 @@ import { chainConnectionAtom, termsIndexAgreedUponAtom, smartWalletProvisionedAtom, - provisionToastIdAtom, networkConfigPAtom, rpcNodeAtom, apiNodeAtom, @@ -34,8 +33,6 @@ import TermsDialog, { currentTermsIndex } from './TermsDialog'; import clsx from 'clsx'; import { makeAgoricChainStorageWatcher } from '@agoric/rpc'; import { sample } from 'lodash-es'; -import ProvisionSmartWalletDialog from './ProvisionSmartWalletDialog'; -import { querySwingsetParams } from 'utils/swingsetParams'; import { loadable } from 'jotai/utils'; import 'react-toastify/dist/ReactToastify.css'; @@ -44,41 +41,6 @@ import SettingsButton from './SettingsButton'; const autoCloseDelayMs = 7000; -const useSmartWalletFeeQuery = () => { - const [smartWalletFee, setFee] = useState(null); - const [error, setError] = useState(null); - const networkConfig = useAtomValue(loadable(networkConfigPAtom)); - - useEffect(() => { - if (networkConfig.state === 'loading') { - return; - } - if (networkConfig.state === 'hasError') { - setError(networkConfig.error); - } - const fetchParams = async (rpc: string) => { - try { - const params = await querySwingsetParams(rpc); - console.debug('swingset params', params); - setFee(BigInt(params.params.powerFlagFees[0].fee[0].amount)); - } catch (e: any) { - setError(e); - } - }; - - if (networkConfig.state === 'hasData') { - const rpc = sample(networkConfig.data.rpcAddrs); - if (!rpc) { - setError('No RPC available in network config'); - } else { - fetchParams(rpc); - } - } - }, [networkConfig]); - - return { smartWalletFee, error }; -}; - const ChainConnection = () => { const [connectionInProgress, setConnectionInProgress] = useState(false); const [chainConnection, setChainConnection] = useAtom(chainConnectionAtom); @@ -88,16 +50,9 @@ const ChainConnection = () => { const setMetricsIndex = useSetAtom(metricsIndexAtom); const setGovernedParamsIndex = useSetAtom(governedParamsIndexAtom); const setInstanceIds = useSetAtom(instanceIdsAtom); - const [provisionToastId, setProvisionToastId] = useAtom(provisionToastIdAtom); - const smartWalletProvisionRequired = useRef(false); - const [isSmartWalletProvisioned, setSmartWalletProvisioned] = useAtom( - smartWalletProvisionedAtom - ); + const setSmartWalletProvisioned = useSetAtom(smartWalletProvisionedAtom); const termsAgreed = useAtomValue(termsIndexAgreedUponAtom); const [isTermsDialogOpen, setIsTermsDialogOpen] = useState(false); - const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); - const { smartWalletFee, error: smartWalletFeeError } = - useSmartWalletFeeQuery(); const networkConfig = useAtomValue(loadable(networkConfigPAtom)); const setRpcNode = useSetAtom(rpcNodeAtom); const setApiNode = useSetAtom(apiNodeAtom); @@ -112,38 +67,6 @@ const ChainConnection = () => { connect(false); }; - useEffect(() => { - if ( - isSmartWalletProvisioned === false && - !smartWalletProvisionRequired.current - ) { - if (smartWalletFeeError) { - console.error('Swingset params error', smartWalletFeeError); - toast.error('Error reading smart wallet provisioning fee from chain.'); - return; - } else if (smartWalletFee) { - smartWalletProvisionRequired.current = true; - setIsProvisionDialogOpen(true); - } - } else if ( - isSmartWalletProvisioned && - smartWalletProvisionRequired.current - ) { - smartWalletProvisionRequired.current = false; - if (provisionToastId) { - toast.dismiss(provisionToastId); - setProvisionToastId(undefined); - } - toast.success('Smart wallet successfully provisioned.'); - } - }, [ - isSmartWalletProvisioned, - provisionToastId, - setProvisionToastId, - smartWalletFeeError, - smartWalletFee, - ]); - useEffect(() => { if (!chainConnection) return; @@ -279,11 +202,6 @@ const ChainConnection = () => { isOpen={isTermsDialogOpen} onClose={handleTermsDialogClose} /> - setIsProvisionDialogOpen(false)} - smartWalletFee={smartWalletFee} - /> ); }; diff --git a/src/components/ProvisionSmartWalletDialog.tsx b/src/components/ProvisionSmartWalletDialog.tsx index 795294b..e85d30c 100644 --- a/src/components/ProvisionSmartWalletDialog.tsx +++ b/src/components/ProvisionSmartWalletDialog.tsx @@ -1,110 +1,79 @@ -import { Dialog, Transition } from '@headlessui/react'; -import clsx from 'clsx'; -import { Fragment } from 'react'; -import { chainConnectionAtom, provisionToastIdAtom } from 'store/app'; -import { useAtomValue, useSetAtom } from 'jotai'; -import { provisionSmartWallet } from 'services/wallet'; +import { useEffect, useState } from 'react'; +import { rpcNodeAtom } from 'store/app'; +import { useAtomValue } from 'jotai'; +import { querySwingsetParams } from 'utils/swingsetParams'; +import ActionsDialog from './ActionsDialog'; // Increment every time the current terms change. export const currentTermsIndex = 1; -const feeDenom = 10n ** 6n; +const useSmartWalletFeeQuery = (rpc?: string) => { + const [smartWalletFee, setFee] = useState(null); + const [error, setError] = useState(null); -const ProvisionSmartWalletDialog = ({ - isOpen, - onClose, - smartWalletFee, -}: { + useEffect(() => { + const fetchParams = async () => { + assert(rpc); + try { + const params = await querySwingsetParams(rpc); + console.debug('swingset params', params); + const beansPerSmartWallet = params.params.beansPerUnit.find( + ({ key }: { key: string }) => key === 'smartWalletProvision' + )?.beans; + const feeUnit = params.params.beansPerUnit.find( + ({ key }: { key: string }) => key === 'feeUnit' + )?.beans; + setFee(BigInt(beansPerSmartWallet) / BigInt(feeUnit)); + } catch (e) { + setError(e as Error); + } + }; + + if (rpc) { + fetchParams(); + } + }, [rpc]); + + return { smartWalletFee, error }; +}; +type Props = { + onConfirm: () => void; isOpen: boolean; onClose: () => void; - smartWalletFee: bigint | null; -}) => { - const chainConnection = useAtomValue(chainConnectionAtom); - const setProvisionToastId = useSetAtom(provisionToastIdAtom); +}; + +const ProvisionSmartWalletDialog = ({ onConfirm, isOpen, onClose }: Props) => { + const rpc = useAtomValue(rpcNodeAtom); + const { smartWalletFee, error: _smartWalletFeeError } = + useSmartWalletFeeQuery(rpc); + const smartWalletFeeForDisplay = smartWalletFee - ? smartWalletFee / feeDenom + ' BLD' + ? smartWalletFee + ' IST' : null; - const provision = () => { - assert(chainConnection); - provisionSmartWallet(chainConnection, setProvisionToastId); - onClose(); - }; + const body = ( + + To interact with contracts on the Agoric chain, a smart wallet must be + created for your account. As an anti-spam measure, you will need{' '} + {smartWalletFeeForDisplay && {smartWalletFeeForDisplay}} to fund + its provision which will be deposited into the reserve pool. Click + "Proceed" to provision wallet and submit transaction. + + ); return ( - - - -
- -
-
- - - - Smart Wallet Required - -
- To interact with contracts on the Agoric chain, a smart wallet - must be created for your account. As an anti-spam measure, you - will need{' '} - {smartWalletFeeForDisplay && ( - {smartWalletFeeForDisplay} - )}{' '} - to fund its provision which will be deposited to the community - fund. -
-
- - -
-
-
-
-
-
-
+ ); }; diff --git a/src/components/Swap.tsx b/src/components/Swap.tsx index e63ea5e..4f57a1a 100644 --- a/src/components/Swap.tsx +++ b/src/components/Swap.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { FiRepeat, FiHelpCircle } from 'react-icons/fi'; import clsx from 'clsx'; @@ -13,7 +13,6 @@ import { instanceIdsAtom, chainConnectionAtom, smartWalletProvisionedAtom, - provisionToastIdAtom, } from 'store/app'; import { clearAmountInputsAtom, instanceAtom } from 'store/swap'; import { @@ -31,10 +30,11 @@ import { errorsAtom, } from 'store/swap'; import { makeSwapOffer } from 'services/swap'; -import { provisionSmartWallet } from 'services/wallet'; import DialogSwap from './DialogSwap'; +import ProvisionSmartWalletDialog from './ProvisionSmartWalletDialog'; const Swap = () => { + const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); const chainConnection = useAtomValue(chainConnectionAtom); const isSmartWalletProvisioned = useAtomValue(smartWalletProvisionedAtom); const brandToInfo = useAtomValue(brandToInfoAtom); @@ -54,7 +54,6 @@ const Swap = () => { const { mintLimit } = useAtomValue(governedParamsAtom) ?? {}; const { anchorPoolBalance, mintedPoolBalance } = useAtomValue(metricsAtom) ?? {}; - const setProvisionToastId = useSetAtom(provisionToastIdAtom); const anchorPetnames = [...instanceIds.keys()]; const areAnchorsLoaded = @@ -75,12 +74,13 @@ const Swap = () => { } }, [swapDirection, setSwapDirection]); - const provision = () => { - assert(chainConnection); - provisionSmartWallet(chainConnection, setProvisionToastId); + const showProvisionDialog = () => { + setIsProvisionDialogOpen(true); }; const handleSwap = useCallback(async () => { + setIsProvisionDialogOpen(false); + if (!areAnchorsLoaded || !chainConnection) return; const fromValue = fromAmount?.value; @@ -234,20 +234,24 @@ const Swap = () => { : 'text-gray-500 cursor-not-allowed' )} disabled={isSmartWalletProvisioned === undefined} - onClick={isSmartWalletProvisioned === false ? provision : handleSwap} + onClick={ + isSmartWalletProvisioned === false + ? showProvisionDialog + : handleSwap + } > -
-
- {isSmartWalletProvisioned === false - ? 'Provision Smart Wallet' - : 'Swap'} -
+
Swap
{errorsToRender} + setIsProvisionDialogOpen(false)} + onConfirm={handleSwap} + /> ); }; diff --git a/src/services/wallet.ts b/src/services/wallet.ts deleted file mode 100644 index c405811..0000000 --- a/src/services/wallet.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { toast } from 'react-toastify'; -import type { Id as ToastId } from 'react-toastify'; -import type { ChainConnection } from 'store/app'; - -export const provisionSmartWallet = async ( - chainConnection: ChainConnection, - setProvisionToastId: (id: ToastId | undefined) => void -) => { - const toastId = toast.info('Provisioning smart wallet...', { - isLoading: true, - }); - setProvisionToastId(toastId); - - try { - await chainConnection.provisionSmartWallet(); - } catch (e: any) { - console.error('Provisioning error', e); - toast.dismiss(toastId); - setProvisionToastId(undefined); - toast.error(`Error provisioning smart wallet: ${e.message}`); - } -}; diff --git a/src/store/app.ts b/src/store/app.ts index d8902fa..e3c5958 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -7,7 +7,6 @@ import { mapAtom } from 'utils/helpers'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio'; import { makeAgoricWalletConnection } from '@agoric/web-components'; import type { Brand, DisplayInfo, Amount } from '@agoric/ertp/src/types'; -import type { Id as ToastId } from 'react-toastify'; import { ChainStorageWatcher } from '@agoric/rpc'; import { loadNetworkConfig } from 'utils/networkConfig'; @@ -71,8 +70,6 @@ export const displayFunctionsAtom = atom(get => { /** Experimental feature flag. */ export const previewEnabledAtom = atom(_get => false); -export const provisionToastIdAtom = atom(undefined); - export const smartWalletProvisionedAtom = atom(undefined); export const chainConnectionErrorInternalAtom = atom(null);