Skip to content

Commit b2239c9

Browse files
committed
[MNY-214] Dashboard: ERC1155 set claim conditions step UX improvements (#8146)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces functionality for handling insufficient funds when creating NFTs, enhancing the user experience by allowing users to buy funds directly if their balance is low. ### Detailed summary - Added `onNotEnoughFunds` callback to handle insufficient funds. - Implemented logic to calculate total transaction costs. - Integrated `BuyWidget` to enable users to purchase funds directly. - Enhanced error handling and user feedback for fund-related issues. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Gas-cost–aware NFT minting with total cost estimation and balance pre-checks. - Insufficient funds handling with a user-facing prompt showing required vs. available balance. - Integrated Buy Funds widget with theme and chain awareness, plus retry and back controls. - Optional transaction path that skips the pay modal in eligible scenarios. - Enhanced batch processing for ERC1155/721 flows with a callback to surface funding issues. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ca701a4 commit b2239c9

File tree

3 files changed

+277
-37
lines changed

3 files changed

+277
-37
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/_common/form.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export type CreateNFTCollectionFunctions = {
7474
count: number;
7575
};
7676
gasless: boolean;
77+
onNotEnoughFunds: (data: {
78+
requiredAmount: string;
79+
balance: string;
80+
}) => void;
7781
}) => Promise<void>;
7882
lazyMintNFTs: (params: {
7983
values: CreateNFTCollectionAllValues;

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/create-nft-page.tsx

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
"use client";
22
import { useRef } from "react";
3-
import { encode, getContract, type ThirdwebClient } from "thirdweb";
3+
import {
4+
encode,
5+
estimateGasCost,
6+
getContract,
7+
getGasPrice,
8+
type PreparedTransaction,
9+
type ThirdwebClient,
10+
toEther,
11+
} from "thirdweb";
412
import { deployERC721Contract, deployERC1155Contract } from "thirdweb/deploys";
513
import { multicall } from "thirdweb/extensions/common";
614
import {
@@ -14,7 +22,8 @@ import {
1422
} from "thirdweb/extensions/erc1155";
1523
import { grantRole } from "thirdweb/extensions/permissions";
1624
import { useActiveAccount } from "thirdweb/react";
17-
import { maxUint256 } from "thirdweb/utils";
25+
import { maxUint256, resolvePromisedValue } from "thirdweb/utils";
26+
import { getWalletBalance } from "thirdweb/wallets";
1827
import { create7702MinimalAccount } from "thirdweb/wallets/smart";
1928
import { revalidatePathAction } from "@/actions/revalidate";
2029
import { reportContractDeployed } from "@/analytics/report";
@@ -39,6 +48,10 @@ export function CreateNFTPage(props: {
3948
const contractAddressRef = useRef<string | undefined>(undefined);
4049
const getChain = useGetV5DashboardChain();
4150
const sendAndConfirmTx = useSendAndConfirmTx();
51+
const sendAndConfirmTxNoPayModal = useSendAndConfirmTx({
52+
payModal: false,
53+
});
54+
4255
function getAccount(params: { gasless: boolean }) {
4356
if (!activeAccount) {
4457
throw new Error("Wallet is not connected");
@@ -218,6 +231,10 @@ export function CreateNFTPage(props: {
218231
count: number;
219232
};
220233
gasless: boolean;
234+
onNotEnoughFunds: (data: {
235+
requiredAmount: string;
236+
balance: string;
237+
}) => void;
221238
}) {
222239
const { values, batch } = params;
223240
const contract = getDeployedContract({
@@ -277,7 +294,44 @@ export function CreateNFTPage(props: {
277294
data: encodedTransactions,
278295
});
279296

280-
await sendAndConfirmTx.mutateAsync(tx);
297+
// if there are more than one batches, on the first batch, we check if the user has enough funds to cover the cost of all the batches ->
298+
// calculate the cost of one batch, multiply by the number of batches to get the total cost
299+
// if the user does not have enough funds, call the onNotEnoughFunds callback
300+
301+
const totalBatches = Math.ceil(values.nfts.length / batch.count);
302+
303+
if (batch.startIndex === 0 && totalBatches > 1 && !params.gasless) {
304+
if (!activeAccount) {
305+
throw new Error("Wallet is not connected");
306+
}
307+
308+
const costPerBatch = await getTotalTransactionCost({
309+
tx: tx,
310+
from: activeAccount.address,
311+
});
312+
313+
const totalCost = costPerBatch * BigInt(totalBatches);
314+
315+
const walletBalance = await getWalletBalance({
316+
address: activeAccount.address,
317+
chain: contract.chain,
318+
client: contract.client,
319+
});
320+
321+
if (walletBalance.value < totalCost) {
322+
params.onNotEnoughFunds({
323+
balance: toEther(walletBalance.value),
324+
requiredAmount: toEther(totalCost),
325+
});
326+
throw new Error(
327+
`Not enough funds: Required ${toEther(totalCost)}, Balance ${toEther(walletBalance.value)}`,
328+
);
329+
}
330+
331+
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
332+
} else {
333+
await sendAndConfirmTx.mutateAsync(tx);
334+
}
281335
}
282336

283337
async function handleSetAdmins(params: {
@@ -390,3 +444,41 @@ function transformSocialUrls(
390444
{} as Record<string, string>,
391445
);
392446
}
447+
448+
async function getTransactionGasCost(tx: PreparedTransaction, from?: string) {
449+
try {
450+
const gasCost = await estimateGasCost({
451+
from,
452+
transaction: tx,
453+
});
454+
455+
const bufferCost = gasCost.wei / 10n;
456+
457+
// Note: get tx.value AFTER estimateGasCost
458+
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
459+
return gasCost.wei + bufferCost;
460+
} catch {
461+
if (from) {
462+
// try again without passing from
463+
return await getTransactionGasCost(tx);
464+
}
465+
// fallback if both fail, use the tx value + 1M * gas price
466+
const gasPrice = await getGasPrice({
467+
chain: tx.chain,
468+
client: tx.client,
469+
});
470+
471+
return 1_000_000n * gasPrice;
472+
}
473+
}
474+
475+
async function getTotalTransactionCost(params: {
476+
tx: PreparedTransaction;
477+
from?: string;
478+
}) {
479+
const [txValue, txGasCost] = await Promise.all([
480+
resolvePromisedValue(params.tx.value),
481+
getTransactionGasCost(params.tx, params.from),
482+
]);
483+
return (txValue || 0n) + txGasCost;
484+
}

0 commit comments

Comments
 (0)