Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto-provision smart wallet #119

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 2 additions & 84 deletions src/components/ChainConnection.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,7 +16,6 @@ import {
chainConnectionAtom,
termsIndexAgreedUponAtom,
smartWalletProvisionedAtom,
provisionToastIdAtom,
networkConfigPAtom,
rpcNodeAtom,
apiNodeAtom,
Expand All @@ -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';
Expand All @@ -44,41 +41,6 @@ import SettingsButton from './SettingsButton';

const autoCloseDelayMs = 7000;

const useSmartWalletFeeQuery = () => {
const [smartWalletFee, setFee] = useState<bigint | null>(null);
const [error, setError] = useState<unknown | null>(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);
Expand All @@ -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);
Expand All @@ -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;

Expand Down Expand Up @@ -279,11 +202,6 @@ const ChainConnection = () => {
isOpen={isTermsDialogOpen}
onClose={handleTermsDialogClose}
/>
<ProvisionSmartWalletDialog
isOpen={isProvisionDialogOpen}
onClose={() => setIsProvisionDialogOpen(false)}
smartWalletFee={smartWalletFee}
/>
</div>
);
};
Expand Down
159 changes: 64 additions & 95 deletions src/components/ProvisionSmartWalletDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<bigint | null>(null);
const [error, setError] = useState<Error | null>(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 = (
<span>
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 && <b>{smartWalletFeeForDisplay}</b>} to fund
its provision which will be deposited into the reserve pool. Click
&quot;Proceed&quot; to provision wallet and submit transaction.
</span>
);

return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Smart Wallet Required
</Dialog.Title>
<div className="mt-2 p-1 max-h-96 overflow-y-auto">
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 && (
<b>{smartWalletFeeForDisplay}</b>
)}{' '}
to fund its provision which will be deposited to the community
fund.
</div>
<div className="mt-4 float-right">
<button
type="button"
className={clsx(
'inline-flex justify-center rounded-md border border-transparent',
'px-4 py-2 text-sm font-medium focus:outline-none focus-visible:ring-2',
'focus-visible:ring-purple-500 focus-visible:ring-offset-2',
'bg-gray-100 text-gray-500 hover:bg-gray-200 mx-4'
)}
onClick={onClose}
>
Back to App
</button>
<button
type="button"
className={clsx(
'inline-flex justify-center rounded-md border border-transparent',
'px-4 py-2 text-sm font-medium focus:outline-none focus-visible:ring-2',
'focus-visible:ring-purple-500 focus-visible:ring-offset-2',
'bg-purple-100 text-purple-900 hover:bg-purple-200'
)}
onClick={provision}
>
Provision Now
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
<ActionsDialog
body={body}
isOpen={isOpen}
title="Smart Wallet Required"
primaryAction={{ label: 'Proceed', action: onConfirm }}
secondaryAction={{
label: 'Go Back',
action: onClose,
}}
onClose={onClose}
initialFocusPrimary={true}
/>
);
};

Expand Down
32 changes: 18 additions & 14 deletions src/components/Swap.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,7 +13,6 @@ import {
instanceIdsAtom,
chainConnectionAtom,
smartWalletProvisionedAtom,
provisionToastIdAtom,
} from 'store/app';
import { clearAmountInputsAtom, instanceAtom } from 'store/swap';
import {
Expand All @@ -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);
Expand All @@ -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 =
Expand All @@ -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;
Expand Down Expand Up @@ -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
}
>
<motion.div className="relative flex flex-row w-full justify-center items-center">
<div className="w-6" />
<div className="text-white w-fit">
{isSmartWalletProvisioned === false
? 'Provision Smart Wallet'
: 'Swap'}
</div>
<div className="text-white w-fit">Swap</div>
</motion.div>
</motion.button>
{errorsToRender}
</motion.div>
<DialogSwap />
<ProvisionSmartWalletDialog
isOpen={isProvisionDialogOpen}
onClose={() => setIsProvisionDialogOpen(false)}
onConfirm={handleSwap}
/>
</>
);
};
Expand Down
Loading