Skip to content
Open
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
220 changes: 176 additions & 44 deletions src/contexts/WalletConnectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { LoadingButton } from '@/components/ui/loading-button';
import { FeeAmountInput } from '@/components/ui/masked-input';
import { Switch } from '@/components/ui/switch';
import { useWallet } from '@/contexts/WalletContext';
import { useBiometric } from '@/hooks/useBiometric';
import { useDefaultFee } from '@/hooks/useDefaultFee';
import { useErrors } from '@/hooks/useErrors';
import { decodeHexMessage, fromMojos, isHex } from '@/lib/utils';
import { decodeHexMessage, fromMojos, toMojos, isHex } from '@/lib/utils';
import { useWalletState } from '@/state';
import {
Params,
Expand Down Expand Up @@ -378,6 +380,112 @@ interface RequestDialogProps {

interface CommandDialogProps<T extends WalletConnectCommand> {
params: Partial<Params<T>>;
onFeeChange?: (feeMojos: string | null) => void;
}

function EditableFee({
suggestedFeeMojos,
onFeeChange,
}: {
suggestedFeeMojos: string | number | undefined;
onFeeChange?: (feeMojos: string | null) => void;
}) {
const walletState = useWalletState();
const { fee: defaultFee } = useDefaultFee();
const precision = walletState.sync.unit.precision;

const suggestedDisplay = fromMojos(
suggestedFeeMojos || 0,
precision,
).toString();
const [feeValue, setFeeValue] = useState<string>(suggestedDisplay);
const [isEditing, setIsEditing] = useState(false);

const handleFeeChange = (values: {
floatValue: number | undefined;
value: string;
}) => {
setFeeValue(values.value);
if (onFeeChange) {
onFeeChange(toMojos(values.value || '0', precision));
}
};

const applyDefaultFee = () => {
setFeeValue(defaultFee);
setIsEditing(true);
if (onFeeChange) {
onFeeChange(toMojos(defaultFee || '0', precision));
}
};

if (!isEditing) {
return (
<div>
<div className='font-medium'>
<Trans>Fee</Trans>{' '}
<span className='text-xs text-muted-foreground'>
(<Trans>suggested by dApp</Trans>)
</span>
</div>
<div className='flex items-center gap-2 mt-1'>
<div className='text-sm text-muted-foreground'>
{formatNumber({
value: fromMojos(suggestedFeeMojos || 0, precision),
minimumFractionDigits: 0,
maximumFractionDigits: precision,
})}{' '}
{walletState.sync.unit.ticker}
</div>
<Button
variant='outline'
size='sm'
className='h-6 text-xs'
onClick={() => setIsEditing(true)}
>
<Trans>Edit</Trans>
</Button>
{parseFloat(defaultFee) > 0 && (
<Button
variant='outline'
size='sm'
className='h-6 text-xs'
onClick={applyDefaultFee}
>
<Trans>Use default</Trans>
</Button>
)}
</div>
</div>
);
}

return (
<div>
<div className='font-medium'>
<Trans>Fee</Trans>
</div>
<div className='flex items-center gap-2 mt-1'>
<div className='flex-1'>
<FeeAmountInput value={feeValue} onValueChange={handleFeeChange} />
</div>
<Button
variant='ghost'
size='sm'
className='h-8 text-xs'
onClick={() => {
setFeeValue(suggestedDisplay);
setIsEditing(false);
if (onFeeChange) {
onFeeChange(null);
}
}}
>
<Trans>Reset</Trans>
</Button>
</div>
</div>
);
}

function SignCoinSpendsDialog({
Expand Down Expand Up @@ -479,7 +587,10 @@ function SignMessageByAddressDialog({
);
}

function TakeOfferDialog({ params }: CommandDialogProps<'chia_takeOffer'>) {
function TakeOfferDialog({
params,
onFeeChange,
}: CommandDialogProps<'chia_takeOffer'>) {
const [offer, setOffer] = useState<OfferSummary | null>(null);
const { addError } = useErrors();

Expand All @@ -490,16 +601,26 @@ function TakeOfferDialog({ params }: CommandDialogProps<'chia_takeOffer'>) {
.catch(addError);
}, [params, addError]);

return offer ? (
<OfferCard summary={offer} />
) : (
<div className='p-4 text-center'>
<Trans>Loading offer details...</Trans>
return (
<div className='space-y-4'>
{offer ? (
<OfferCard summary={offer} />
) : (
<div className='p-4 text-center'>
<Trans>Loading offer details...</Trans>
</div>
)}
<div className='px-4 pb-4'>
<EditableFee suggestedFeeMojos={params.fee} onFeeChange={onFeeChange} />
</div>
</div>
);
}

function CreateOfferDialog({ params }: CommandDialogProps<'chia_createOffer'>) {
function CreateOfferDialog({
params,
onFeeChange,
}: CommandDialogProps<'chia_createOffer'>) {
const walletState = useWalletState();
// Check if any requested assets are revocable
const hasRevocableAssets = params.requestAssets?.some(
Expand Down Expand Up @@ -563,23 +684,15 @@ function CreateOfferDialog({ params }: CommandDialogProps<'chia_createOffer'>) {
))}
</ul>
</div>
<div>
<div className='font-medium'>Fee</div>
<div className='text-sm text-muted-foreground'>
{formatNumber({
value: fromMojos(params.fee || 0, walletState.sync.unit.precision),
minimumFractionDigits: 0,
maximumFractionDigits: walletState.sync.unit.precision,
})}{' '}
{walletState.sync.unit.ticker}
</div>
</div>
<EditableFee suggestedFeeMojos={params.fee} onFeeChange={onFeeChange} />
</div>
);
}

function CancelOfferDialog({ params }: CommandDialogProps<'chia_cancelOffer'>) {
const walletState = useWalletState();
function CancelOfferDialog({
params,
onFeeChange,
}: CommandDialogProps<'chia_cancelOffer'>) {
const [record, setRecord] = useState<OfferRecord | null>(null);
const { addError } = useErrors();

Expand All @@ -595,15 +708,7 @@ function CancelOfferDialog({ params }: CommandDialogProps<'chia_cancelOffer'>) {
<div className='font-medium'>Offer ID</div>
<div className='text-sm text-muted-foreground'>{params.id}</div>

<div className='font-medium'>Fee</div>
<div className='text-sm text-muted-foreground'>
{formatNumber({
value: fromMojos(params.fee || 0, walletState.sync.unit.precision),
minimumFractionDigits: 0,
maximumFractionDigits: walletState.sync.unit.precision,
})}{' '}
{walletState.sync.unit.ticker}
</div>
<EditableFee suggestedFeeMojos={params.fee} onFeeChange={onFeeChange} />

{record && (
<div className='border rounded-md'>
Expand All @@ -614,7 +719,7 @@ function CancelOfferDialog({ params }: CommandDialogProps<'chia_cancelOffer'>) {
);
}

function SendDialog({ params }: CommandDialogProps<'chia_send'>) {
function SendDialog({ params, onFeeChange }: CommandDialogProps<'chia_send'>) {
const walletState = useWalletState();

return (
Expand All @@ -641,17 +746,7 @@ function SendDialog({ params }: CommandDialogProps<'chia_send'>) {
{params.assetId ? 'CAT' : walletState.sync.unit.ticker}
</div>
</div>
<div>
<div className='font-medium'>Fee</div>
<div className='text-sm text-muted-foreground'>
{formatNumber({
value: fromMojos(params.fee || 0, walletState.sync.unit.precision),
minimumFractionDigits: 0,
maximumFractionDigits: walletState.sync.unit.precision,
})}{' '}
{walletState.sync.unit.ticker}
</div>
</div>
<EditableFee suggestedFeeMojos={params.fee} onFeeChange={onFeeChange} />
{params.assetId && (
<div>
<div className='font-medium'>Asset Id</div>
Expand Down Expand Up @@ -685,6 +780,14 @@ const COMMAND_COMPONENTS: {
chia_signMessageByAddress: SignMessageByAddressDialog,
};

const COMMANDS_WITH_FEE = new Set<string>([
'chia_send',
'chia_createOffer',
'chia_cancelOffer',
'chia_takeOffer',
// 'chia_bulkMintNfts', // not implemented yet, but will require fee override support when it is
]);

const COMMAND_METADATA: Partial<
Record<
WalletConnectCommand,
Expand Down Expand Up @@ -727,6 +830,7 @@ function RequestDialog({
signClient,
}: RequestDialogProps) {
const [isApproving, setIsApproving] = useState(false);
const [feeOverride, setFeeOverride] = useState<string | null>(null);
const { currentTheme } = useTheme();
const method = request.params.request.method as WalletConnectCommand;
const params = request.params.request.params;
Expand All @@ -744,6 +848,29 @@ function RequestDialog({
[params, commandInfo],
);

const hasFee = COMMANDS_WITH_FEE.has(method);

const approveWithFee = useCallback(async () => {
if (feeOverride !== null && hasFee) {
const modifiedRequest = {
...request,
params: {
...request.params,
request: {
...request.params.request,
params: {
...request.params.request.params,
fee: feeOverride,
},
},
},
};
await approve(modifiedRequest);
} else {
await approve(request);
}
}, [request, approve, feeOverride, hasFee]);

if (!commandInfo.confirm) {
return null;
}
Expand Down Expand Up @@ -779,7 +906,12 @@ function RequestDialog({
</DialogHeader>

<div className='max-h-[60vh] overflow-y-auto mb-2'>
{CommandComponent && <CommandComponent params={parsedParams ?? {}} />}
{CommandComponent && (
<CommandComponent
params={parsedParams ?? {}}
onFeeChange={hasFee ? setFeeOverride : undefined}
/>
)}
</div>

<DialogFooter>
Expand All @@ -794,7 +926,7 @@ function RequestDialog({
onClick={async () => {
setIsApproving(true);
try {
await approve(request);
await approveWithFee();
} finally {
setIsApproving(false);
}
Expand Down
Loading