From 34f6c9a18a7dd9d6de46db0358e5f81a4e469561 Mon Sep 17 00:00:00 2001 From: itttm127 Date: Thu, 23 Jan 2025 01:50:48 +0900 Subject: [PATCH 1/3] chore: added the padding on listing, sales, transfer table and updated empty case after loading && update error system on stamp mint page --- client/hooks/useSRC20Form.ts | 13 +- components/shared/fee/FeeCalculatorBase.tsx | 8 +- components/stampDetails/StampListingsAll.tsx | 1 + components/stampDetails/StampSales.tsx | 1 + components/stampDetails/StampTransfers.tsx | 1 + components/tokenDetails/TokenMints.tsx | 1 + components/tokenDetails/TokenTransfers.tsx | 1 + islands/shared/Tables.tsx | 3 +- islands/stamping/stamp/OlgaContent.tsx | 3 + lib/utils/apiResponseUtil.ts | 2 +- routes/api/v2/src20/create.ts | 2 +- .../services/src20/psbt/src20PSBTService.ts | 219 +++++++++--------- server/services/transaction/utxoService.ts | 95 ++++---- server/services/xcpService.ts | 38 +-- 14 files changed, 200 insertions(+), 188 deletions(-) diff --git a/client/hooks/useSRC20Form.ts b/client/hooks/useSRC20Form.ts index e4bc15efe..cdabcbee9 100644 --- a/client/hooks/useSRC20Form.ts +++ b/client/hooks/useSRC20Form.ts @@ -599,15 +599,10 @@ export function useSRC20Form( error: error instanceof Error ? error.message : String(error), details: error, }); - - if (error instanceof Error) { - const apiError = (error as any).response?.data?.error; - setApiError( - apiError || error.message || "An unexpected error occurred", - ); - } else { - setApiError("An unexpected error occurred"); - } + const apiError = (error as any).response?.data?.error; + setApiError( + apiError || error.message || "An unexpected error occurred", + ); } finally { setIsSubmitting(false); } diff --git a/components/shared/fee/FeeCalculatorBase.tsx b/components/shared/fee/FeeCalculatorBase.tsx index 572fcf482..af172cfd5 100644 --- a/components/shared/fee/FeeCalculatorBase.tsx +++ b/components/shared/fee/FeeCalculatorBase.tsx @@ -224,11 +224,11 @@ export function FeeCalculatorBase({ {/* Sats Per Byte */}

- SATS PER BYTE {/* {fee} */} + SATS PER BYTE {fee}

{/* Miner Fee */} - {feeDetails?.minerFee && ( + {!!feeDetails?.minerFee && (

MINER FEE {coinType === "BTC" ? formatSatoshisToBTC(feeDetails.minerFee, { @@ -263,7 +263,7 @@ export function FeeCalculatorBase({ )} {/* Dust Value */} - {feeDetails?.dustValue && feeDetails.dustValue > 0 && ( + {!!feeDetails?.dustValue && (

DUST {coinType === "BTC" ? formatSatoshisToBTC(feeDetails.dustValue, { @@ -275,7 +275,7 @@ export function FeeCalculatorBase({ )} {/* Total */} - {feeDetails?.totalValue && ( + {!!feeDetails?.totalValue && (

TOTAL {coinType === "BTC" ? formatSatoshisToBTC(feeDetails.totalValue, { diff --git a/components/stampDetails/StampListingsAll.tsx b/components/stampDetails/StampListingsAll.tsx index b77d37e8f..8dcf2ba4b 100644 --- a/components/stampDetails/StampListingsAll.tsx +++ b/components/stampDetails/StampListingsAll.tsx @@ -61,6 +61,7 @@ export function StampListingsAll({ dispensers }: StampListingsAllProps) { {header} ))} + )} diff --git a/components/stampDetails/StampSales.tsx b/components/stampDetails/StampSales.tsx index 344bf144f..9876a2e88 100644 --- a/components/stampDetails/StampSales.tsx +++ b/components/stampDetails/StampSales.tsx @@ -47,6 +47,7 @@ export function StampSales({ dispenses }: StampSalesProps) { {header} ))} + )} diff --git a/components/stampDetails/StampTransfers.tsx b/components/stampDetails/StampTransfers.tsx index b14b2f266..c156f5b4c 100644 --- a/components/stampDetails/StampTransfers.tsx +++ b/components/stampDetails/StampTransfers.tsx @@ -43,6 +43,7 @@ export function StampTransfers({ sends }: StampTransfersProps) { {header} ))} + )} diff --git a/components/tokenDetails/TokenMints.tsx b/components/tokenDetails/TokenMints.tsx index a8b560a28..65279176d 100644 --- a/components/tokenDetails/TokenMints.tsx +++ b/components/tokenDetails/TokenMints.tsx @@ -35,6 +35,7 @@ export function TokenMints({ mints }: TokenMintsProps) { {header} ))} + )} diff --git a/components/tokenDetails/TokenTransfers.tsx b/components/tokenDetails/TokenTransfers.tsx index f9a492ebd..b071b8629 100644 --- a/components/tokenDetails/TokenTransfers.tsx +++ b/components/tokenDetails/TokenTransfers.tsx @@ -36,6 +36,7 @@ export function TokenTransfers({ sends }: TokenTransfersProps) { {header} ))} + )} diff --git a/islands/shared/Tables.tsx b/islands/shared/Tables.tsx index a428e2be7..1726d0567 100644 --- a/islands/shared/Tables.tsx +++ b/islands/shared/Tables.tsx @@ -28,7 +28,7 @@ export default function Table({ }: TableProps) { const [selectedTab, setSelectedTab] = useState(configs[0].id); const [tabData, setTabData] = useState({}); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [totalCounts, setTotalCounts] = useState(initialCounts); @@ -51,7 +51,6 @@ export default function Table({ tabId: string, isTabChange = false, ) => { - if (isLoading) return; if (!isTabChange && !hasMore) return; setIsLoading(true); diff --git a/islands/stamping/stamp/OlgaContent.tsx b/islands/stamping/stamp/OlgaContent.tsx index d385864fa..06c813b69 100644 --- a/islands/stamping/stamp/OlgaContent.tsx +++ b/islands/stamping/stamp/OlgaContent.tsx @@ -194,6 +194,9 @@ function extractErrorMessage(error: unknown): string { path: "error.message", value: err.response?.data?.error, }); + if (err.response?.data?.error?.includes("Insufficient funds")) { + return "Insufficient funds to cover outputs and fees"; + } return err.response?.data?.error; } diff --git a/lib/utils/apiResponseUtil.ts b/lib/utils/apiResponseUtil.ts index 583eb0163..c0b367233 100644 --- a/lib/utils/apiResponseUtil.ts +++ b/lib/utils/apiResponseUtil.ts @@ -231,7 +231,7 @@ export class ApiResponseUtil { static internalError( error: unknown, - message = "Internal server error", + message: string = "Internal server error", options: ApiResponseOptions = {}, ): Response { console.error("Internal Error:", error); diff --git a/routes/api/v2/src20/create.ts b/routes/api/v2/src20/create.ts index 4ee768b86..b75cab3ee 100644 --- a/routes/api/v2/src20/create.ts +++ b/routes/api/v2/src20/create.ts @@ -128,7 +128,7 @@ export const handler: Handlers = { message: "Error processing request", error: error instanceof Error ? error.message : String(error), }); - return ApiResponseUtil.internalError(error); + return ApiResponseUtil.internalError(error, error as string); } }, }; diff --git a/server/services/src20/psbt/src20PSBTService.ts b/server/services/src20/psbt/src20PSBTService.ts index 3fbc24130..c2cfe15ae 100644 --- a/server/services/src20/psbt/src20PSBTService.ts +++ b/server/services/src20/psbt/src20PSBTService.ts @@ -98,121 +98,124 @@ export class SRC20PSBTService { action: src20Action.op, } }); - - const effectiveChangeAddress = changeAddress || sourceAddress; - const network = networks.bitcoin; - - // 1. First prepare the action data and get CIP33 addresses - const { chunks } = await this.prepareActionData(src20Action); - - // 2. Construct all outputs with their exact scripts and values - const outputs = [ - // First output is the recipient - { - script: address.toOutputScript(toAddress, network), - value: this.DUST_SIZE, - }, - // Then add all CIP33 data outputs - ...chunks.map((chunk, i) => ({ - script: address.toOutputScript(chunk, network), - value: this.DUST_SIZE + i, - })) - ]; - - // 3. Calculate total output value needed - const totalOutputValue = outputs.reduce((sum, out) => sum + out.value, 0); - - // 4. Select UTXOs with the target fee rate - const { inputs, change } = await TransactionService.UTXOService.selectUTXOsForTransaction( - sourceAddress, - outputs, - satsPerVB, - 0, - 1.5, - { filterStampUTXOs: true } - ); - - // 5. Create PSBT with actual inputs and outputs - const psbt = new Psbt({ network }); - - // Add inputs - for (const input of inputs) { - const txDetails = await QuicknodeService.getTransaction(input.txid); - if (!txDetails) { - throw new Error(`Failed to fetch transaction details for ${input.txid}`); + try { + const effectiveChangeAddress = changeAddress || sourceAddress; + const network = networks.bitcoin; + + // 1. First prepare the action data and get CIP33 addresses + const { chunks } = await this.prepareActionData(src20Action); + + // 2. Construct all outputs with their exact scripts and values + const outputs = [ + // First output is the recipient + { + script: address.toOutputScript(toAddress, network), + value: this.DUST_SIZE, + }, + // Then add all CIP33 data outputs + ...chunks.map((chunk, i) => ({ + script: address.toOutputScript(chunk, network), + value: this.DUST_SIZE + i, + })) + ]; + + // 3. Calculate total output value needed + const totalOutputValue = outputs.reduce((sum, out) => sum + out.value, 0); + + // 4. Select UTXOs with the target fee rate + const { inputs, change } = await TransactionService.UTXOService.selectUTXOsForTransaction( + sourceAddress, + outputs, + satsPerVB, + 0, + 1.5, + { filterStampUTXOs: true } + ); + + // 5. Create PSBT with actual inputs and outputs + const psbt = new Psbt({ network }); + + // Add inputs + for (const input of inputs) { + const txDetails = await QuicknodeService.getTransaction(input.txid); + if (!txDetails) { + throw new Error(`Failed to fetch transaction details for ${input.txid}`); + } + psbt.addInput(this.createPsbtInput(input, txDetails)); } - psbt.addInput(this.createPsbtInput(input, txDetails)); - } - - // Add all outputs - outputs.forEach(output => { - psbt.addOutput({ - script: output.script, - value: BigInt(output.value) + + // Add all outputs + outputs.forEach(output => { + psbt.addOutput({ + script: output.script, + value: BigInt(output.value) + }); }); - }); - - // Add change output if needed - if (change > this.DUST_SIZE) { - psbt.addOutput({ - script: address.toOutputScript(effectiveChangeAddress, network), - value: BigInt(change) + + // Add change output if needed + if (change > this.DUST_SIZE) { + psbt.addOutput({ + script: address.toOutputScript(effectiveChangeAddress, network), + value: BigInt(change) + }); + } + + // 6. Calculate actual transaction fee from input/output values + const totalInputValue = inputs.reduce((sum, input) => sum + Number(input.value), 0); + const totalOutputAmount = outputs.reduce((sum, out) => sum + out.value, 0) + + (change > this.DUST_SIZE ? change : 0); + const actualFee = totalInputValue - totalOutputAmount; + const dustTotal = outputs.reduce((sum, out) => sum + out.value, 0); + + // Log exact values for debugging + logger.debug("stamps", { + message: "Transaction details", + data: { + totalInputValue, + totalOutputAmount, + actualFee, + dustTotal, + changeAmount: change, + feeBreakdown: { + minerFee: actualFee, + dustValue: dustTotal, + total: actualFee + dustTotal + }, + outputs: outputs.map(o => o.value), + change, + inputValues: inputs.map(i => i.value) + } }); - } - - // 6. Calculate actual transaction fee from input/output values - const totalInputValue = inputs.reduce((sum, input) => sum + Number(input.value), 0); - const totalOutputAmount = outputs.reduce((sum, out) => sum + out.value, 0) + - (change > this.DUST_SIZE ? change : 0); - const actualFee = totalInputValue - totalOutputAmount; - const dustTotal = outputs.reduce((sum, out) => sum + out.value, 0); - - // Log exact values for debugging - logger.debug("stamps", { - message: "Transaction details", - data: { + + return { + psbt, + estimatedTxSize: Math.ceil(actualFee / satsPerVB), totalInputValue, - totalOutputAmount, - actualFee, - dustTotal, - changeAmount: change, - feeBreakdown: { + totalOutputValue: totalOutputAmount, + totalChangeOutput: change, + totalDustValue: dustTotal, + estMinerFee: actualFee, + feeDetails: { + baseFee: actualFee, + ancestorFee: 0, + effectiveFeeRate: satsPerVB, + ancestorCount: 0, + totalVsize: Math.ceil(actualFee / satsPerVB), + total: actualFee + dustTotal, minerFee: actualFee, dustValue: dustTotal, - total: actualFee + dustTotal + totalValue: actualFee + dustTotal }, - outputs: outputs.map(o => o.value), - change, - inputValues: inputs.map(i => i.value) - } - }); - - return { - psbt, - estimatedTxSize: Math.ceil(actualFee / satsPerVB), - totalInputValue, - totalOutputValue: totalOutputAmount, - totalChangeOutput: change, - totalDustValue: dustTotal, - estMinerFee: actualFee, - feeDetails: { - baseFee: actualFee, - ancestorFee: 0, - effectiveFeeRate: satsPerVB, - ancestorCount: 0, - totalVsize: Math.ceil(actualFee / satsPerVB), - total: actualFee + dustTotal, - minerFee: actualFee, - dustValue: dustTotal, - totalValue: actualFee + dustTotal - }, - changeAddress: effectiveChangeAddress, - inputs: inputs.map((input, index) => ({ - index, - address: input.address, - sighashType: 1 - })) - }; + changeAddress: effectiveChangeAddress, + inputs: inputs.map((input, index) => ({ + index, + address: input.address, + sighashType: 1 + })) + }; + } catch (error) { + throw error.message + } } private static async prepareActionData(src20Action: string | object) { diff --git a/server/services/transaction/utxoService.ts b/server/services/transaction/utxoService.ts index 01e698b2c..bf34e66ea 100644 --- a/server/services/transaction/utxoService.ts +++ b/server/services/transaction/utxoService.ts @@ -191,55 +191,58 @@ export class UTXOService { // Sort using BigInt comparison utxosWithValues.sort((a, b) => Number(b.effectiveValue - a.effectiveValue)); // Convert back to number for sort comparison - - for (const utxo of utxosWithValues) { - selectedInputs.push(utxo); - totalInputValue += BigInt(utxo.value); - - // Calculate current fee with proper script type detection - const currentFee = BigInt(calculateMiningFee( - selectedInputs.map(input => { - const scriptType = getScriptTypeInfo(input.script); - return { - type: scriptType.type, - size: scriptType.size, - isWitness: scriptType.isWitness, - ancestor: input.ancestor - }; - }), - vouts.map(output => { - const scriptType = output.script ? - getScriptTypeInfo(output.script) : - { type: "P2WPKH", size: TX_CONSTANTS.P2WPKH.size, isWitness: true }; - return { - type: scriptType.type, - size: scriptType.size, - isWitness: scriptType.isWitness, - value: Number(output.value) // Keep as number for fee calculation - }; - }), - feeRate, - { - includeChangeOutput: true, - changeOutputType: "P2WPKH" - } - )); - - if (totalInputValue >= totalOutputValue + currentFee) { - const change = totalInputValue - totalOutputValue - currentFee; - const changeDust = BigInt(this.CHANGE_DUST); - - if (change >= changeDust || change === BigInt(0)) { - return { - inputs: selectedInputs, - change: change >= changeDust ? Number(change) : 0, // Convert back to number - fee: Number(currentFee), // Convert back to number - }; + try { + for (const utxo of utxosWithValues) { + selectedInputs.push(utxo); + totalInputValue += BigInt(utxo.value); + + // Calculate current fee with proper script type detection + const currentFee = BigInt(calculateMiningFee( + selectedInputs.map(input => { + const scriptType = getScriptTypeInfo(input.script); + return { + type: scriptType.type, + size: scriptType.size, + isWitness: scriptType.isWitness, + ancestor: input.ancestor + }; + }), + vouts.map(output => { + const scriptType = output.script ? + getScriptTypeInfo(output.script) : + { type: "P2WPKH", size: TX_CONSTANTS.P2WPKH.size, isWitness: true }; + return { + type: scriptType.type, + size: scriptType.size, + isWitness: scriptType.isWitness, + value: Number(output.value) // Keep as number for fee calculation + }; + }), + feeRate, + { + includeChangeOutput: true, + changeOutputType: "P2WPKH" + } + )); + + if (totalInputValue >= totalOutputValue + currentFee) { + const change = totalInputValue - totalOutputValue - currentFee; + const changeDust = BigInt(this.CHANGE_DUST); + + if (change >= changeDust || change === BigInt(0)) { + return { + inputs: selectedInputs, + change: change >= changeDust ? Number(change) : 0, // Convert back to number + fee: Number(currentFee), // Convert back to number + }; + } } } + + throw new Error("Insufficient funds to cover outputs and fees"); + } catch (error) { + throw error } - - throw new Error("Insufficient funds to cover outputs and fees"); } private static isWitnessInput(script: string): boolean { diff --git a/server/services/xcpService.ts b/server/services/xcpService.ts index 677bda67c..f6e7bb99f 100644 --- a/server/services/xcpService.ts +++ b/server/services/xcpService.ts @@ -44,25 +44,29 @@ export function normalizeFeeRate(params: { } { let normalizedSatsPerVB: number; - if (params.satsPerVB !== undefined) { - normalizedSatsPerVB = params.satsPerVB; - } else if (params.satsPerKB !== undefined) { - // If satsPerKB/1000 < 1, assume it was intended as sats/vB - normalizedSatsPerVB = params.satsPerKB < SATS_PER_KB_MULTIPLIER - ? params.satsPerKB - : params.satsPerKB / SATS_PER_KB_MULTIPLIER; - } else { - throw new Error("Either satsPerKB or satsPerVB must be provided"); - } + try { + if (params.satsPerVB !== undefined) { + normalizedSatsPerVB = params.satsPerVB; + } else if (params.satsPerKB !== undefined) { + // If satsPerKB/1000 < 1, assume it was intended as sats/vB + normalizedSatsPerVB = params.satsPerKB < SATS_PER_KB_MULTIPLIER + ? params.satsPerKB + : params.satsPerKB / SATS_PER_KB_MULTIPLIER; + } else { + throw new Error("Either satsPerKB or satsPerVB must be provided"); + } - if (normalizedSatsPerVB <= 2) { - throw new Error("Fee rate must be greater than 2 sat/vB"); - } + if (normalizedSatsPerVB <= 2) { + throw new Error("Fee rate must be greater than 2 sat/vB"); + } - return { - normalizedSatsPerVB, - normalizedSatsPerKB: normalizedSatsPerVB * SATS_PER_KB_MULTIPLIER - }; + return { + normalizedSatsPerVB, + normalizedSatsPerKB: normalizedSatsPerVB * SATS_PER_KB_MULTIPLIER + }; + } catch (error) { + throw error.message + } } export async function fetchXcpV2WithCache( From cf7750e69a002643b29d8ae18545588db3e4b1f7 Mon Sep 17 00:00:00 2001 From: itttm127 Date: Thu, 23 Jan 2025 23:31:37 +0900 Subject: [PATCH 2/3] chore: add successful notification on deploy & transfer and update wanring text --- client/hooks/useSRC20Form.ts | 10 ++++++---- deno.json | 1 + .../stamping/src20/deploy/DeployContent.tsx | 3 ++- lib/utils/notificationUtil.ts | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 lib/utils/notificationUtil.ts diff --git a/client/hooks/useSRC20Form.ts b/client/hooks/useSRC20Form.ts index cdabcbee9..d9f8eedc5 100644 --- a/client/hooks/useSRC20Form.ts +++ b/client/hooks/useSRC20Form.ts @@ -7,6 +7,7 @@ import { fetchBTCPriceInUSD } from "$lib/utils/balanceUtils.ts"; import { Config } from "$globals"; import { logger } from "$lib/utils/logger.ts"; import { debounce } from "$lib/utils/debounce.ts"; +import { showNotification } from "$lib/utils/notificationUtils.ts"; interface PSBTFees { estMinerFee: number; totalDustValue: number; @@ -503,10 +504,11 @@ export function useSRC20Form( ); if (walletResult.signed) { - setSubmissionMessage({ - message: "Transaction broadcasted successfully.", - txid: walletResult.txid, - }); + showNotification( + "Transaction Successfully.", + walletResult.txid, + "success", + ); } else if (walletResult.cancelled) { setSubmissionMessage({ message: "Transaction signing cancelled by user.", diff --git a/deno.json b/deno.json index 5a1834d67..540c60f02 100644 --- a/deno.json +++ b/deno.json @@ -109,6 +109,7 @@ "redis": "https://deno.land/x/redis@v0.32.4/mod.ts", "swiper/": "https://esm.sh/swiper@11.1.14/", "swiper": "https://esm.sh/swiper@11.1.14", + "sweetalert2": "https://esm.sh/sweetalert2@11.15.10", "tailwindcss": "npm:tailwindcss@3.4.1", "tailwindcss/": "npm:tailwindcss@3.4.1/", "tailwindcss/plugin": "npm:tailwindcss@3.4.1/plugin.js", diff --git a/islands/stamping/src20/deploy/DeployContent.tsx b/islands/stamping/src20/deploy/DeployContent.tsx index 697ff1dd9..e7b8ae712 100644 --- a/islands/stamping/src20/deploy/DeployContent.tsx +++ b/islands/stamping/src20/deploy/DeployContent.tsx @@ -127,7 +127,8 @@ export function DeployContent( }); setFileUploadError( - `File upload failed: ${errorMessage}. The deployment will continue without the background image.`, + // `File upload failed: ${errorMessage}. The deployment will continue without the background image.`, + `File upload failed: The deployment will continue without the background image.`, ); } }; diff --git a/lib/utils/notificationUtil.ts b/lib/utils/notificationUtil.ts new file mode 100644 index 000000000..e11748bb7 --- /dev/null +++ b/lib/utils/notificationUtil.ts @@ -0,0 +1,18 @@ +import Swal from "sweetalert2"; + +const ButtonClassNames = + "inline-flex items-center justify-center border-2 border-solid border-stamp-purple rounded-md text-sm mobileLg:text-base font-extrabold text-stamp-purple tracking-[0.05em] h-[42px] mobileLg:h-[48px] px-4 mobileLg:px-5 hover:border-stamp-purple-highlight hover:text-stamp-purple-highlight transition-colors"; + +export function showNotification(title: string, text: string, icon: string) { + Swal.fire({ + title: title || "Notification", + text: text || "", + icon: icon || "info", + background: + "linear-gradient(to bottom right,#1f002e00,#14001f7f,#1f002e),#000", + confirmButtonText: "O K", + customClass: { + confirmButton: ButtonClassNames, + }, + }); +} From 22f15f7fedf3f8a5eca812e26d23faa5743862c3 Mon Sep 17 00:00:00 2001 From: itttm127 Date: Thu, 23 Jan 2025 23:39:10 +0900 Subject: [PATCH 3/3] chore: fix code quality --- lib/utils/{notificationUtil.ts => notificationUtils.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/utils/{notificationUtil.ts => notificationUtils.ts} (100%) diff --git a/lib/utils/notificationUtil.ts b/lib/utils/notificationUtils.ts similarity index 100% rename from lib/utils/notificationUtil.ts rename to lib/utils/notificationUtils.ts