Skip to content

Commit c45a9c9

Browse files
feat(dapp): add balance validation to auctions (#400)
* feat(dapp): add balance validation to auctions * feat(dapp): move balance validation to a hook * fix validation * comments * fix balance validation --------- Co-authored-by: evavirseda <evirseda@boxfish.studio>
1 parent 2015bba commit c45a9c9

File tree

7 files changed

+169
-46
lines changed

7 files changed

+169
-46
lines changed

dapp/src/auctions/components/dialogs/AuctionBidDialog.tsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ import { useAuctionBid } from '@/auctions/hooks/useAuctionBid';
3434
import { useCountdown } from '@/auctions/hooks/useCountdown';
3535
import { useGetAuctionMetadata } from '@/auctions/hooks/useGetAuctionMetadata';
3636
import { formatTimeRemaining, getTimeRemaining, getUserAuctionStatus } from '@/auctions/lib/utils';
37-
import { NameRecordData, queryKey, useNameRecord } from '@/hooks';
37+
import { NameRecordData, queryKey, useBalanceValidation, useNameRecord } from '@/hooks';
38+
import {
39+
GAS_BALANCE_TOO_LOW_ID,
40+
GAS_BUDGET_ERROR_MESSAGES,
41+
INSUFFICIENT_COIN_BALANCE_ID,
42+
NOT_ENOUGH_BALANCE_ID,
43+
} from '@/lib/constants';
3844
import { formatNanosToIota } from '@/lib/utils';
3945
import { toNanos } from '@/lib/utils/amount';
4046
import { formatExpirationDate } from '@/lib/utils/format/formatExpirationDate';
@@ -76,11 +82,17 @@ export function AuctionBidDialog({ name, closeDialog, onCompleted }: AuctionBidD
7682
data: auctionBidTransaction,
7783
isLoading: isAuctionBidLoading,
7884
isPending: isAuctionBidPending,
79-
error,
85+
error: auctionError,
8086
} = useAuctionBid({
8187
name,
8288
bidNanos: bidNanos ?? BigInt(0),
8389
});
90+
91+
const { data: balanceValidation, error: balanceValidationError } = useBalanceValidation(
92+
auctionBidTransaction?.builtTx ?? null,
93+
Number(bidNanos),
94+
);
95+
8496
const { mutateAsync: signAndExecuteTransaction, isPending: isSendingTransaction } =
8597
useSignAndExecuteTransaction();
8698

@@ -104,6 +116,11 @@ export function AuctionBidDialog({ name, closeDialog, onCompleted }: AuctionBidD
104116
},
105117
});
106118

119+
const hasEnoughGas =
120+
!balanceValidationError?.message.includes(NOT_ENOUGH_BALANCE_ID) &&
121+
!balanceValidationError?.message.includes(GAS_BALANCE_TOO_LOW_ID) &&
122+
!balanceValidationError?.message.includes(INSUFFICIENT_COIN_BALANCE_ID);
123+
107124
const status = auctionMetadata && getUserAuctionStatus(auctionMetadata, account?.address || '');
108125
const timeRemainingMs = auctionMetadata && getTimeRemaining(auctionMetadata);
109126
const { milliseconds } = useCountdown(timeRemainingMs || 0);
@@ -114,7 +131,12 @@ export function AuctionBidDialog({ name, closeDialog, onCompleted }: AuctionBidD
114131
const isLoading =
115132
isNameRecordLoading || isAuctionBidLoading || isSendingTransaction || isSigningTransaction;
116133
const isPending = isAuctionBidPending;
117-
const disablePlaceBid = isPending || isLoading || isBidBelowMinimum;
134+
const disablePlaceBid =
135+
isPending ||
136+
isLoading ||
137+
isBidBelowMinimum ||
138+
!balanceValidation?.hasBalance ||
139+
!hasEnoughGas;
118140

119141
const formattedTimeRemaining = formatTimeRemaining(milliseconds);
120142
const currentBid = auctionMetadata
@@ -143,8 +165,12 @@ export function AuctionBidDialog({ name, closeDialog, onCompleted }: AuctionBidD
143165
return `The value exceeds the maximum decimals (${IOTA_DECIMALS}).`;
144166
} else if (isBidBelowMinimum) {
145167
return `Bid must be ≥ ${minBidLabel}`;
146-
} else if (error) {
147-
return error.message;
168+
} else if (!hasEnoughGas) {
169+
return GAS_BUDGET_ERROR_MESSAGES[NOT_ENOUGH_BALANCE_ID];
170+
} else if (auctionError) {
171+
return auctionError.message;
172+
} else if (balanceValidationError) {
173+
return balanceValidationError.message;
148174
}
149175
})();
150176

@@ -193,7 +219,7 @@ export function AuctionBidDialog({ name, closeDialog, onCompleted }: AuctionBidD
193219
</div>
194220
)}
195221
<Input
196-
type={InputType.Number}
222+
type={InputType.NumericFormat}
197223
label="Your Bid"
198224
min={Number(minBidNanos)}
199225
value={bidAmountValue}
@@ -226,7 +252,7 @@ export function AuctionBidDialog({ name, closeDialog, onCompleted }: AuctionBidD
226252
text={auctionMetadata ? 'Bid' : 'Start auction'}
227253
onClick={() => {
228254
if (auctionBidTransaction) {
229-
handleConfirm(auctionBidTransaction);
255+
handleConfirm(auctionBidTransaction.transaction);
230256
}
231257
}}
232258
fullWidth

dapp/src/auctions/hooks/useAuctionBid.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ export function useAuctionBid({ name, bidNanos }: UseActionBidParams) {
6161
name,
6262
);
6363

64-
await transaction.build({
64+
const tx = await transaction.build({
6565
client: iotaClient,
6666
});
67-
return transaction;
67+
return {
68+
transaction,
69+
builtTx: tx,
70+
};
6871
},
6972
enabled: enableAuctionBid,
7073
});

dapp/src/components/dialogs/PurchaseNameDialog.tsx

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
'use client';
55

6+
import { Warning } from '@iota/apps-ui-icons';
67
import {
78
Button,
89
ButtonType,
@@ -13,6 +14,9 @@ import {
1314
DialogPosition,
1415
DisplayStats,
1516
Header,
17+
InfoBox,
18+
InfoBoxStyle,
19+
InfoBoxType,
1620
LoadingIndicator,
1721
Panel,
1822
Select,
@@ -23,8 +27,7 @@ import { normalizeIotaName } from '@iota/iota-names-sdk';
2327
import { useMutation, useQueryClient } from '@tanstack/react-query';
2428
import { useState } from 'react';
2529

26-
import { NameUpdate, queryKey, useUpdateNameTransaction } from '@/hooks';
27-
import { useBalance } from '@/hooks/useBalance';
30+
import { NameUpdate, queryKey, useBalanceValidation, useUpdateNameTransaction } from '@/hooks';
2831
import { useCoreConfig } from '@/hooks/useCoreConfig';
2932
import { useNameRecord } from '@/hooks/useNameRecord';
3033
import {
@@ -86,11 +89,14 @@ export function PurchaseNameDialog({ name, open, setOpen, onPurchase }: Purchase
8689
updates: updates,
8790
});
8891

92+
const { data: balanceValidation, error: balanceValidationError } = useBalanceValidation(
93+
updateNameData?.builtTx ?? null,
94+
price,
95+
);
96+
8997
const { mutateAsync: signAndExecuteTransaction, isPending: isSendingTransaction } =
9098
useSignAndExecuteTransaction();
9199

92-
const { data: coinBalance, error: coinBalanceError } = useBalance(account?.address ?? '');
93-
94100
const {
95101
mutateAsync: handlePurchase,
96102
error: purchaseError,
@@ -130,19 +136,17 @@ export function PurchaseNameDialog({ name, open, setOpen, onPurchase }: Purchase
130136
}))
131137
: [];
132138

133-
const totalBalance = Number(coinBalance?.totalBalance) || 0;
134-
const totalGas = Number(updateNameData?.gasSummary?.totalGas) || 0;
135-
const totalPrice = nameRecordData?.type === 'available' ? nameRecordData.price + totalGas : 0;
136-
const hasBalance = totalBalance > totalPrice;
137-
138139
const hasEnoughGas =
139140
!updateNameError?.message.includes(NOT_ENOUGH_BALANCE_ID) &&
140141
!updateNameError?.message.includes(GAS_BALANCE_TOO_LOW_ID);
141142

142143
const canPay =
143-
isConnected && hasEnoughGas && hasBalance && nameRecordData?.type === 'available';
144+
isConnected &&
145+
hasEnoughGas &&
146+
balanceValidation?.hasBalance &&
147+
nameRecordData?.type === 'available';
144148

145-
const hasErrors = updateNameError || coinBalanceError || purchaseError;
149+
const hasErrors = updateNameError || balanceValidationError || purchaseError;
146150

147151
const isLoading = isNameRecordLoading || isUpdateNameLoading || isSigning;
148152

@@ -186,12 +190,63 @@ export function PurchaseNameDialog({ name, open, setOpen, onPurchase }: Purchase
186190
</div>
187191
</Panel>
188192
<div className="flex flex-row gap-x-sm w-full">
189-
<DisplayStats label="Registration Expires" value={expirationDate} />
190-
<DisplayStats
191-
label="Total Due"
192-
value={formatNanosToIota(totalPrice)}
193-
/>
193+
{!isLoading &&
194+
balanceValidation &&
195+
typeof balanceValidation?.totalPrice === 'number' &&
196+
balanceValidation.totalPrice > 0 && (
197+
<>
198+
<DisplayStats
199+
label="Registration Expires"
200+
value={expirationDate}
201+
/>
202+
<DisplayStats
203+
label="Total Due"
204+
value={formatNanosToIota(
205+
balanceValidation.totalPrice,
206+
)}
207+
/>
208+
</>
209+
)}
194210
</div>
211+
212+
{!hasEnoughGas && (
213+
<InfoBox
214+
title="Error"
215+
supportingText={
216+
GAS_BUDGET_ERROR_MESSAGES[GAS_BALANCE_TOO_LOW_ID]
217+
}
218+
icon={<Warning />}
219+
type={InfoBoxType.Error}
220+
style={InfoBoxStyle.Default}
221+
/>
222+
)}
223+
{purchaseError && (
224+
<InfoBox
225+
title="Error"
226+
supportingText={purchaseError.message}
227+
icon={<Warning />}
228+
type={InfoBoxType.Error}
229+
style={InfoBoxStyle.Default}
230+
/>
231+
)}
232+
{nameRecordError && (
233+
<InfoBox
234+
title="Error"
235+
supportingText={nameRecordError.message}
236+
icon={<Warning />}
237+
type={InfoBoxType.Error}
238+
style={InfoBoxStyle.Default}
239+
/>
240+
)}
241+
{hasEnoughGas && updateNameError && (
242+
<InfoBox
243+
title="Error"
244+
supportingText={updateNameError.message}
245+
icon={<Warning />}
246+
type={InfoBoxType.Error}
247+
style={InfoBoxStyle.Default}
248+
/>
249+
)}
195250
<div className="flex w-full flex-row gap-x-xs mt-xs">
196251
<Button
197252
type={ButtonType.Secondary}
@@ -209,26 +264,6 @@ export function PurchaseNameDialog({ name, open, setOpen, onPurchase }: Purchase
209264
/>
210265
</div>
211266
</div>
212-
{!hasEnoughGas && (
213-
<div className="text-center text-red-400 text-sm">
214-
{GAS_BUDGET_ERROR_MESSAGES[GAS_BALANCE_TOO_LOW_ID]}
215-
</div>
216-
)}
217-
{purchaseError && (
218-
<div className="text-center text-red-400 text-sm">
219-
{purchaseError.message}
220-
</div>
221-
)}
222-
{nameRecordError && (
223-
<div className="text-center text-red-400 text-sm">
224-
{nameRecordError.message}
225-
</div>
226-
)}
227-
{hasEnoughGas && updateNameError && (
228-
<div className="text-center text-red-400 text-sm">
229-
{updateNameError.message}
230-
</div>
231-
)}
232267
</div>
233268
</DialogBody>
234269
</DialogContent>

dapp/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './usePriceList';
1010
export * from './queryKey';
1111
export * from './useGetDefaultName';
1212
export * from './useUpdateNameTransaction';
13+
export * from './useBalanceValidation';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { useCurrentAccount, useIotaClient } from '@iota/dapp-kit';
5+
import { useQuery } from '@tanstack/react-query';
6+
7+
import { getGasSummary } from '@/lib/utils/getGasSummary';
8+
9+
import { useBalance } from './useBalance';
10+
11+
export function useBalanceValidation(
12+
builtTx: Uint8Array<ArrayBufferLike> | null,
13+
price: number | null,
14+
) {
15+
const client = useIotaClient();
16+
const account = useCurrentAccount();
17+
const { data: coinBalance, error: coinBalanceError } = useBalance(account?.address ?? '');
18+
return useQuery({
19+
queryKey: [builtTx, price, client, coinBalance, coinBalanceError, account?.address],
20+
queryFn: async () => {
21+
if (!builtTx || !price) {
22+
return {
23+
totalGas: 0,
24+
totalPrice: 0,
25+
hasBalance: false,
26+
gasSummary: '',
27+
coinBalanceError: undefined,
28+
};
29+
}
30+
const txDryRun = await client.dryRunTransactionBlock({
31+
transactionBlock: builtTx,
32+
});
33+
const totalGas = Number(getGasSummary(txDryRun)?.totalGas);
34+
const totalPrice = Number(price) + totalGas;
35+
const totalBalance = Number(coinBalance?.totalBalance);
36+
const hasBalance = totalBalance > totalPrice;
37+
return {
38+
totalGas,
39+
totalPrice,
40+
hasBalance,
41+
coinBalanceError,
42+
};
43+
},
44+
enabled: !!price && price > 0,
45+
gcTime: 0,
46+
select: ({ totalGas, totalPrice, hasBalance, coinBalanceError }) => {
47+
return {
48+
totalGas,
49+
totalPrice,
50+
hasBalance,
51+
coinBalanceError,
52+
};
53+
},
54+
});
55+
}

dapp/src/hooks/useUpdateNameTransaction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,17 @@ export function useUpdateNameTransaction({ address, updates }: UseUpdateNameTran
173173
});
174174
return {
175175
transaction: iotaNamesTx.transaction,
176+
builtTx: transaction,
176177
txDryRun,
177178
};
178179
},
179180
enabled: !!address && !!updates.length,
180181
gcTime: 0,
181-
select: ({ transaction, txDryRun }) => {
182+
select: ({ transaction, txDryRun, builtTx }) => {
182183
return {
183184
transaction,
184185
gasSummary: getGasSummary(txDryRun),
186+
builtTx,
185187
};
186188
},
187189
});

dapp/src/lib/constants/errorMessages.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
export const GAS_BALANCE_TOO_LOW_ID = 'GasBalanceTooLow';
55
export const NOT_ENOUGH_BALANCE_ID = 'No valid gas coins found';
6+
export const INSUFFICIENT_COIN_BALANCE_ID = 'InsufficientCoinBalance';
67

78
export const GAS_BUDGET_ERROR_MESSAGES = {
89
[GAS_BALANCE_TOO_LOW_ID]: 'Not enough balance to cover transaction fees.',

0 commit comments

Comments
 (0)