fix: transaction button gas estimation for pay (#3178)

This commit is contained in:
Joaquim Verges
2024-05-30 00:31:40 +00:00
committed by GitHub
parent fd777c71c2
commit 3dca028a7e
7 changed files with 131 additions and 40 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---
Fix gas estimation in transaction button
@@ -3,6 +3,8 @@ import { Button } from "@/components/ui/button";
import { metadataBase } from "@/lib/constants";
import type { Metadata } from "next";
import Link from "next/link";
import { CodeExample } from "../../../components/code/code-example";
import { StyledPayTransaction } from "../../../components/styled-pay-transaction";
export const metadata: Metadata = {
metadataBase,
@@ -55,6 +57,46 @@ export default function Page() {
</div>
</div>
</section>
<section className="container px-4 md:px-6 space-y-8">
<div className="space-y-2">
<h2 className="text-4xl font-semibold tracking-tight">
Transaction Button
</h2>
<p className="max-w-[600px]">
Transaction Button is a handy component that handles transactions.
<br />
If your user doesn&apos;t have enough funds for that transaction, a
pre-filled pay modal will appear with the exact amount needed.
</p>
</div>
<CodeExample
preview={<StyledPayTransaction />}
code={`import { TransactionButton } from "thirdweb/react";
function App() {
const account = useActiveAccount();
return (
<TransactionButton
transaction={() => {
// any transaction works
return claimTo({
contract,
quantity: 1n,
tokenId: 0n,
to: account?.address,
});
}}
>
Buy for 10 MATIC
</TransactionButton>
);
};`}
lang="tsx"
/>
</section>
</main>
);
}
@@ -0,0 +1,38 @@
"use client";
import { THIRDWEB_CLIENT } from "@/lib/client";
import { useTheme } from "next-themes";
import { getContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { claimTo } from "thirdweb/extensions/erc1155";
import { TransactionButton, useActiveAccount } from "thirdweb/react";
const contract = getContract({
address: "0x96B30d36f783c7BC68535De23147e2ce65788e93",
chain: polygon,
client: THIRDWEB_CLIENT,
});
export function StyledPayTransaction() {
const account = useActiveAccount();
const { theme } = useTheme();
return (
<TransactionButton
transaction={() => {
if (!account) throw new Error("No active account");
return claimTo({
contract,
quantity: 1n,
tokenId: 0n,
to: account?.address,
});
}}
payModal={{
theme: theme === "light" ? "light" : "dark",
}}
>
Buy for 10 MATIC
</TransactionButton>
);
}
+5 -18
View File
@@ -1,10 +1,6 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -22,19 +18,10 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
},
"target": "ES2017"
"target": "ES2020"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
@@ -5,7 +5,6 @@ import { sendTransaction } from "../../../../transaction/actions/send-transactio
import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
import { resolvePromisedValue } from "../../../../utils/promise/resolve-promised-value.js";
import type { Account } from "../../../../wallets/interfaces/wallet.js";
import {
type GetWalletBalanceResult,
getWalletBalance,
@@ -105,7 +104,7 @@ export function useSendTransactionCore(
chain: tx.chain,
client: tx.client,
}),
getTotalTxCostForBuy(tx, account),
getTotalTxCostForBuy(tx, account?.address),
]);
const walletBalanceWei = walletBalance.value;
@@ -139,18 +138,31 @@ export function useSendTransactionCore(
export async function getTotalTxCostForBuy(
tx: PreparedTransaction,
account?: Account,
from?: string,
) {
const gasCost = await estimateGasCost({
transaction: tx,
account,
});
try {
const gasCost = await estimateGasCost({
transaction: tx,
from,
});
const bufferCost = gasCost.wei / 10n;
const bufferCost = gasCost.wei / 10n;
// Note: get tx.value AFTER estimateGasCost
const txValue = await resolvePromisedValue(tx.value);
// Note: get tx.value AFTER estimateGasCost
const txValue = await resolvePromisedValue(tx.value);
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
return gasCost.wei + bufferCost + (txValue || 0n);
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
return gasCost.wei + bufferCost + (txValue || 0n);
} catch (e) {
if (from) {
// try again without passing from
return await getTotalTxCostForBuy(tx);
}
// fallback if both fail, use the tx value + 1% buffer
const value = await resolvePromisedValue(tx.value);
if (!value) {
return 0n;
}
return value + value / 100n;
}
}
@@ -38,7 +38,10 @@ export function useBuyTxStates(options: {
}
try {
const totalCost = await getTotalTxCostForBuy(buyForTx.tx, account);
const totalCost = await getTotalTxCostForBuy(
buyForTx.tx,
account?.address,
);
if (!mounted) {
return;
@@ -23,7 +23,10 @@ export type EstimateGasOptions = Prettify<
export type EstimateGasResult = bigint;
const cache = new WeakMap<PreparedTransaction, Promise<EstimateGasResult>>();
const cache = new WeakMap<
PreparedTransaction & { from: string | undefined },
Promise<EstimateGasResult>
>();
/**
* Estimates the gas required to execute a transaction. The gas is returned as a `bigint` and in gwei units.
@@ -41,9 +44,15 @@ const cache = new WeakMap<PreparedTransaction, Promise<EstimateGasResult>>();
export async function estimateGas(
options: EstimateGasOptions,
): Promise<EstimateGasResult> {
if (cache.has(options.transaction)) {
// from is:
// 1. the user specified from address
// 2. the passed in account address
// 3. the passed in wallet's account address
const from = options.from ?? options.account?.address ?? undefined;
const txWithFrom = { ...options.transaction, from };
if (cache.has(txWithFrom)) {
// biome-ignore lint/style/noNonNullAssertion: the `has` above ensures that this will always be set
return cache.get(options.transaction)!;
return cache.get(txWithFrom)!;
}
const { account } = options;
const promise = (async () => {
@@ -84,11 +93,6 @@ export async function estimateGas(
]);
const rpcRequest = getRpcClient(options.transaction);
// from is:
// 1. the user specified from address
// 2. the passed in account address
// 3. the passed in wallet's account address
const from = options.from ?? options.account?.address ?? undefined;
try {
let gas = await eth_estimateGas(
rpcRequest,
@@ -110,6 +114,6 @@ export async function estimateGas(
});
}
})();
cache.set(options.transaction, promise);
cache.set(txWithFrom, promise);
return promise;
}