From e8fcf93c08144bc2ef39b0e66a1808f3de8596a0 Mon Sep 17 00:00:00 2001 From: Pablo Castellano Date: Thu, 24 Apr 2025 23:10:28 +0200 Subject: [PATCH 1/3] feat: enhance topup and create-batch commands with gas checks and improved displays - Add gas (xDAI) and BZZ balance checks to prevent transaction failures - Reorganize output to show all costs before asking for confirmation --- src/command/stamp/topup.ts | 68 ++++++++++++++++++++ src/command/utility/create-batch.ts | 96 ++++++++++++++++++++++++----- 2 files changed, 148 insertions(+), 16 deletions(-) diff --git a/src/command/stamp/topup.ts b/src/command/stamp/topup.ts index f169a3e0..70b047b8 100644 --- a/src/command/stamp/topup.ts +++ b/src/command/stamp/topup.ts @@ -1,7 +1,11 @@ import { LeafCommand, Option } from 'furious-commander' +import { Utils } from '@ethersphere/bee-js' +import { BigNumber, providers, ethers } from 'ethers' import { pickStamp } from '../../service/stamp' import { stampProperties } from '../../utils/option' import { createSpinner } from '../../utils/spinner' +import { NETWORK_ID } from '../../utils/contracts' +import { eth_getBalance } from '../../utils/rpc' import { VerbosityLevel } from '../root-command/command-log' import { StampCommand } from './stamp-command' @@ -29,6 +33,70 @@ export class Topup extends StampCommand implements LeafCommand { this.stamp = await pickStamp(this.bee, this.console) } + // Get stamp details to calculate duration extension + const stamp = await this.bee.getPostageBatch(this.stamp) + const chainState = await this.bee.getChainState() + const { bzzBalance } = await this.bee.getWalletBalance() + + // Calculate duration extension (approximate) + const currentPrice = BigInt(chainState.currentPrice) + const blocksPerDay = 17280n // ~5 seconds per block + const additionalDaysNumber = Number(this.amount) / Number(currentPrice * blocksPerDay) + + // Calculate cost in BZZ + const bzzCost = Utils.getStampCost(stamp.depth, this.amount) + + // Get wallet address + const { ethereum } = await this.bee.getNodeAddresses() + const walletAddress = ethereum.toHex() + const provider = new providers.JsonRpcProvider('https://xdai.fairdatasociety.org', NETWORK_ID) + + // Use a fixed gas estimate instead of dynamic calculation to avoid errors + const gasPrice = await provider.getGasPrice() + const gasLimit = BigNumber.from(100000) // Typical gas limit for token operations + const estimatedGasCost = gasPrice.mul(gasLimit) + + // Display cost information to the user + this.console.log(`Topping up stamp ${this.stamp} with ${this.amount} PLUR (depth: ${stamp.depth})`) + this.console.log(`Current price: ${currentPrice.toString()} PLUR per block`) + this.console.log(`Estimated TTL extension: ~${additionalDaysNumber.toFixed(2)} days`) + this.console.log(`Stamp cost: ${bzzCost.toDecimalString()} BZZ`) + this.console.log(`Gas cost: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI`) + + // We already have the wallet address from above + + // Check if wallet has enough BZZ funds before proceeding + if (bzzBalance.toPLURBigInt() < bzzCost.toPLURBigInt()) { + this.console.error(`\nWallet address: 0x${walletAddress} has insufficient BZZ funds.`) + this.console.error(`Required: ${bzzCost.toDecimalString()} BZZ`) + this.console.error(`Available: ${bzzBalance.toDecimalString()} BZZ`) + process.exit(1) + } + + // Check if wallet has enough gas (xDAI) to pay for transaction fees + const xDAI = await eth_getBalance(walletAddress, provider) + const xDAIValue = BigNumber.from(xDAI) + + if (xDAIValue.lt(estimatedGasCost)) { + this.console.error(`\nWallet address: 0x${walletAddress} has insufficient xDAI funds for gas fees.`) + this.console.error( + `Required: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI, Available: ${ethers.utils.formatEther( + xDAIValue, + )} xDAI`, + ) + process.exit(1) + } + + // Ask for confirmation before proceeding + if (!this.yes) { + this.yes = await this.console.confirm('Do you want to proceed with this topup?') + } + + if (!this.yes) { + this.console.log('Topup cancelled by user') + return + } + const spinner = createSpinner('Topup in progress. This may take a few minutes.') if (this.verbosity !== VerbosityLevel.Quiet && !this.curl) { diff --git a/src/command/utility/create-batch.ts b/src/command/utility/create-batch.ts index 9c2bd603..b9be48f4 100644 --- a/src/command/utility/create-batch.ts +++ b/src/command/utility/create-batch.ts @@ -1,9 +1,9 @@ import { Utils } from '@ethersphere/bee-js' import { Numbers, Strings } from 'cafe-utility' -import { Contract, Event, Wallet } from 'ethers' +import { BigNumber, Contract, Event, ethers, providers, Wallet } from 'ethers' import { LeafCommand, Option } from 'furious-commander' -import { ABI, Contracts } from '../../utils/contracts' -import { makeReadySigner } from '../../utils/rpc' +import { ABI, Contracts, NETWORK_ID } from '../../utils/contracts' +import { eth_getBalance, makeReadySigner } from '../../utils/rpc' import { RootCommand } from '../root-command' export class CreateBatch extends RootCommand implements LeafCommand { @@ -49,6 +49,68 @@ export class CreateBatch extends RootCommand implements LeafCommand { public async run(): Promise { super.init() + const wallet = new Wallet(this.privateKey) + const cost = Utils.getStampCost(this.depth, this.amount) + const signer = await makeReadySigner(wallet.privateKey, this.jsonRpcUrl) + + // Check if wallet has enough BZZ funds before proceeding + const tokenProxyContract = new Contract(Contracts.bzz, ABI.tokenProxy, signer) + const bzzContract = new Contract(Contracts.bzz, ABI.bzz, signer) + const balance = await bzzContract.balanceOf(wallet.address) + + if (balance.lt(cost.toPLURBigInt().toString())) { + this.console.error(`\nWallet address: 0x${wallet.address} has insufficient BZZ funds.`) + this.console.error(`Required: ${cost.toDecimalString()} BZZ`) + this.console.error(`Available: ${Number(balance) / 10 ** 18} BZZ`) + process.exit(1) + } + + // Check if wallet has enough gas (xDAI) to pay for transaction fees + const provider = new providers.JsonRpcProvider(this.jsonRpcUrl, NETWORK_ID) + const xDAI = await eth_getBalance(wallet.address, provider) + const xDAIValue = BigNumber.from(xDAI) + + // Estimate gas costs for approval and batch creation + const gasPrice = await provider.getGasPrice() + const approvalGasLimit = BigNumber.from(130000) + const batchCreationGasLimit = BigNumber.from(1000000) + const totalGasLimit = approvalGasLimit.add(batchCreationGasLimit) + const estimatedGasCost = gasPrice.mul(totalGasLimit) + + if (xDAIValue.lt(estimatedGasCost)) { + this.console.error(`\nWallet address: 0x${wallet.address} has insufficient xDAI funds for gas fees.`) + this.console.error( + `Required: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI, Available: ${ethers.utils.formatEther( + xDAIValue, + )} xDAI`, + ) + process.exit(1) + } + + // Display cost and wait for user confirmation before proceeding + this.console.log(`Creating a batch will cost ${cost.toDecimalString()} BZZ`) + this.console.log(`Gas cost: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI`) + this.console.log(`Your current balance is ${Number(balance) / 10 ** 18} BZZ`) + + // Create a contract instance with allowance method + const allowanceAbi = [ + { + type: 'function', + stateMutability: 'view', + payable: false, + outputs: [{ type: 'uint256', name: 'remaining' }], + name: 'allowance', + inputs: [ + { type: 'address', name: '_owner' }, + { type: 'address', name: '_spender' }, + ], + constant: true, + }, + ] + const bzzAllowanceContract = new Contract(Contracts.bzz, allowanceAbi, signer) + const currentAllowance = await bzzAllowanceContract.allowance(wallet.address, Contracts.postageStamp) + this.console.log(`Current allowance: ${Number(currentAllowance) / 10 ** 18} BZZ`) + if (!this.yes) { this.yes = await this.console.confirm( 'This command creates an external batch for advanced usage. Do you want to continue?', @@ -59,20 +121,22 @@ export class CreateBatch extends RootCommand implements LeafCommand { return } - const wallet = new Wallet(this.privateKey) - const cost = Utils.getStampCost(this.depth, this.amount) - const signer = await makeReadySigner(wallet.privateKey, this.jsonRpcUrl) + // Use the already fetched allowance to determine if approval is necessary + const requiredAmount = cost.toPLURBigInt().toString() - this.console.log(`Approving spending of ${cost.toDecimalString()} BZZ to ${wallet.address}`) - const tokenProxyContract = new Contract(Contracts.bzz, ABI.tokenProxy, signer) - const approve = await tokenProxyContract.approve(Contracts.postageStamp, cost.toPLURBigInt().toString(), { - gasLimit: 130_000, - type: 2, - maxFeePerGas: Numbers.make('2gwei'), - maxPriorityFeePerGas: Numbers.make('1gwei'), - }) - this.console.log(`Waiting 3 blocks on approval tx ${approve.hash}`) - await approve.wait(3) + if (currentAllowance.lt(requiredAmount)) { + this.console.log(`Approving spending of ${cost.toDecimalString()} BZZ to ${wallet.address}`) + const approve = await tokenProxyContract.approve(Contracts.postageStamp, requiredAmount, { + gasLimit: 130_000, + type: 2, + maxFeePerGas: Numbers.make('2gwei'), + maxPriorityFeePerGas: Numbers.make('1gwei'), + }) + this.console.log(`Waiting 3 blocks on approval tx ${approve.hash}`) + await approve.wait(3) + } else { + this.console.log(`Approval not needed. Current allowance: ${Number(currentAllowance) / 10 ** 18} BZZ`) + } this.console.log(`Creating postage batch for ${wallet.address} with depth ${this.depth} and amount ${this.amount}`) const postageStampContract = new Contract(Contracts.postageStamp, ABI.postageStamp, signer) From 62a9546c29707bab1ef1d0f6947813ae56ffd785 Mon Sep 17 00:00:00 2001 From: Pablo Castellano Date: Fri, 25 Apr 2025 16:39:57 +0200 Subject: [PATCH 2/3] refactor: common functions --- src/command/stamp/topup.ts | 61 +++++------ src/command/utility/create-batch.ts | 111 ++++++++------------ src/utils/bzz-transaction-utils.ts | 151 ++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 103 deletions(-) create mode 100644 src/utils/bzz-transaction-utils.ts diff --git a/src/command/stamp/topup.ts b/src/command/stamp/topup.ts index 70b047b8..bf0c6d32 100644 --- a/src/command/stamp/topup.ts +++ b/src/command/stamp/topup.ts @@ -1,13 +1,10 @@ import { LeafCommand, Option } from 'furious-commander' -import { Utils } from '@ethersphere/bee-js' -import { BigNumber, providers, ethers } from 'ethers' import { pickStamp } from '../../service/stamp' import { stampProperties } from '../../utils/option' import { createSpinner } from '../../utils/spinner' -import { NETWORK_ID } from '../../utils/contracts' -import { eth_getBalance } from '../../utils/rpc' import { VerbosityLevel } from '../root-command/command-log' import { StampCommand } from './stamp-command' +import { calculateAndDisplayCosts, checkBzzBalance, checkXdaiBalance } from '../../utils/bzz-transaction-utils' export class Topup extends StampCommand implements LeafCommand { public readonly name = 'topup' @@ -43,47 +40,43 @@ export class Topup extends StampCommand implements LeafCommand { const blocksPerDay = 17280n // ~5 seconds per block const additionalDaysNumber = Number(this.amount) / Number(currentPrice * blocksPerDay) - // Calculate cost in BZZ - const bzzCost = Utils.getStampCost(stamp.depth, this.amount) - // Get wallet address const { ethereum } = await this.bee.getNodeAddresses() const walletAddress = ethereum.toHex() - const provider = new providers.JsonRpcProvider('https://xdai.fairdatasociety.org', NETWORK_ID) + + this.console.log(`Topping up stamp ${this.stamp} of depth ${stamp.depth} with ${this.amount} PLUR.\n`) - // Use a fixed gas estimate instead of dynamic calculation to avoid errors - const gasPrice = await provider.getGasPrice() - const gasLimit = BigNumber.from(100000) // Typical gas limit for token operations - const estimatedGasCost = gasPrice.mul(gasLimit) + // Calculate costs + const { bzzCost, estimatedGasCost } = await calculateAndDisplayCosts( + stamp.depth, + this.amount, + bzzBalance.toPLURBigInt(), + this.console + ) - // Display cost information to the user - this.console.log(`Topping up stamp ${this.stamp} with ${this.amount} PLUR (depth: ${stamp.depth})`) this.console.log(`Current price: ${currentPrice.toString()} PLUR per block`) this.console.log(`Estimated TTL extension: ~${additionalDaysNumber.toFixed(2)} days`) - this.console.log(`Stamp cost: ${bzzCost.toDecimalString()} BZZ`) - this.console.log(`Gas cost: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI`) - - // We already have the wallet address from above - // Check if wallet has enough BZZ funds before proceeding - if (bzzBalance.toPLURBigInt() < bzzCost.toPLURBigInt()) { - this.console.error(`\nWallet address: 0x${walletAddress} has insufficient BZZ funds.`) - this.console.error(`Required: ${bzzCost.toDecimalString()} BZZ`) - this.console.error(`Available: ${bzzBalance.toDecimalString()} BZZ`) + // Check BZZ balance + const hasSufficientBzz = await checkBzzBalance( + walletAddress, + bzzCost.toPLURBigInt(), + bzzBalance.toPLURBigInt(), + this.console + ) + + if (!hasSufficientBzz) { process.exit(1) } - // Check if wallet has enough gas (xDAI) to pay for transaction fees - const xDAI = await eth_getBalance(walletAddress, provider) - const xDAIValue = BigNumber.from(xDAI) - - if (xDAIValue.lt(estimatedGasCost)) { - this.console.error(`\nWallet address: 0x${walletAddress} has insufficient xDAI funds for gas fees.`) - this.console.error( - `Required: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI, Available: ${ethers.utils.formatEther( - xDAIValue, - )} xDAI`, - ) + // Check xDAI balance + const hasSufficientXdai = await checkXdaiBalance( + walletAddress, + estimatedGasCost, + this.console, + ) + + if (!hasSufficientXdai) { process.exit(1) } diff --git a/src/command/utility/create-batch.ts b/src/command/utility/create-batch.ts index b9be48f4..6e21e460 100644 --- a/src/command/utility/create-batch.ts +++ b/src/command/utility/create-batch.ts @@ -1,10 +1,10 @@ -import { Utils } from '@ethersphere/bee-js' import { Numbers, Strings } from 'cafe-utility' -import { BigNumber, Contract, Event, ethers, providers, Wallet } from 'ethers' +import { BigNumber, Contract, Event, Wallet } from 'ethers' import { LeafCommand, Option } from 'furious-commander' -import { ABI, Contracts, NETWORK_ID } from '../../utils/contracts' -import { eth_getBalance, makeReadySigner } from '../../utils/rpc' +import { ABI, Contracts } from '../../utils/contracts' +import { makeReadySigner } from '../../utils/rpc' import { RootCommand } from '../root-command' +import { calculateAndDisplayCosts, checkBzzBalance, checkXdaiBalance, checkAndApproveAllowance } from '../../utils/bzz-transaction-utils' export class CreateBatch extends RootCommand implements LeafCommand { public readonly name = 'create-batch' @@ -50,67 +50,44 @@ export class CreateBatch extends RootCommand implements LeafCommand { super.init() const wallet = new Wallet(this.privateKey) - const cost = Utils.getStampCost(this.depth, this.amount) const signer = await makeReadySigner(wallet.privateKey, this.jsonRpcUrl) - // Check if wallet has enough BZZ funds before proceeding - const tokenProxyContract = new Contract(Contracts.bzz, ABI.tokenProxy, signer) + // Get BZZ balance const bzzContract = new Contract(Contracts.bzz, ABI.bzz, signer) const balance = await bzzContract.balanceOf(wallet.address) + const bzzBalance = BigNumber.from(balance) + + // Calculate costs + const { bzzCost, estimatedGasCost } = await calculateAndDisplayCosts( + this.depth, + this.amount, + bzzBalance.toBigInt(), + this.console + ) - if (balance.lt(cost.toPLURBigInt().toString())) { - this.console.error(`\nWallet address: 0x${wallet.address} has insufficient BZZ funds.`) - this.console.error(`Required: ${cost.toDecimalString()} BZZ`) - this.console.error(`Available: ${Number(balance) / 10 ** 18} BZZ`) + // Check BZZ balance + const hasSufficientBzz = await checkBzzBalance( + wallet.address, + bzzCost.toPLURBigInt(), + bzzBalance.toBigInt(), + this.console + ) + + if (!hasSufficientBzz) { process.exit(1) } - // Check if wallet has enough gas (xDAI) to pay for transaction fees - const provider = new providers.JsonRpcProvider(this.jsonRpcUrl, NETWORK_ID) - const xDAI = await eth_getBalance(wallet.address, provider) - const xDAIValue = BigNumber.from(xDAI) - - // Estimate gas costs for approval and batch creation - const gasPrice = await provider.getGasPrice() - const approvalGasLimit = BigNumber.from(130000) - const batchCreationGasLimit = BigNumber.from(1000000) - const totalGasLimit = approvalGasLimit.add(batchCreationGasLimit) - const estimatedGasCost = gasPrice.mul(totalGasLimit) - - if (xDAIValue.lt(estimatedGasCost)) { - this.console.error(`\nWallet address: 0x${wallet.address} has insufficient xDAI funds for gas fees.`) - this.console.error( - `Required: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI, Available: ${ethers.utils.formatEther( - xDAIValue, - )} xDAI`, - ) + // Check xDAI balance + const hasSufficientXdai = await checkXdaiBalance( + wallet.address, + estimatedGasCost, + this.console + ) + + if (!hasSufficientXdai) { process.exit(1) } - // Display cost and wait for user confirmation before proceeding - this.console.log(`Creating a batch will cost ${cost.toDecimalString()} BZZ`) - this.console.log(`Gas cost: ~${ethers.utils.formatEther(estimatedGasCost)} xDAI`) - this.console.log(`Your current balance is ${Number(balance) / 10 ** 18} BZZ`) - - // Create a contract instance with allowance method - const allowanceAbi = [ - { - type: 'function', - stateMutability: 'view', - payable: false, - outputs: [{ type: 'uint256', name: 'remaining' }], - name: 'allowance', - inputs: [ - { type: 'address', name: '_owner' }, - { type: 'address', name: '_spender' }, - ], - constant: true, - }, - ] - const bzzAllowanceContract = new Contract(Contracts.bzz, allowanceAbi, signer) - const currentAllowance = await bzzAllowanceContract.allowance(wallet.address, Contracts.postageStamp) - this.console.log(`Current allowance: ${Number(currentAllowance) / 10 ** 18} BZZ`) - if (!this.yes) { this.yes = await this.console.confirm( 'This command creates an external batch for advanced usage. Do you want to continue?', @@ -121,21 +98,17 @@ export class CreateBatch extends RootCommand implements LeafCommand { return } - // Use the already fetched allowance to determine if approval is necessary - const requiredAmount = cost.toPLURBigInt().toString() - - if (currentAllowance.lt(requiredAmount)) { - this.console.log(`Approving spending of ${cost.toDecimalString()} BZZ to ${wallet.address}`) - const approve = await tokenProxyContract.approve(Contracts.postageStamp, requiredAmount, { - gasLimit: 130_000, - type: 2, - maxFeePerGas: Numbers.make('2gwei'), - maxPriorityFeePerGas: Numbers.make('1gwei'), - }) - this.console.log(`Waiting 3 blocks on approval tx ${approve.hash}`) - await approve.wait(3) - } else { - this.console.log(`Approval not needed. Current allowance: ${Number(currentAllowance) / 10 ** 18} BZZ`) + // Check and approve allowance if needed + const requiredAmount = bzzCost.toPLURBigInt().toString() + const approved = await checkAndApproveAllowance( + this.privateKey, + requiredAmount, + this.console + ) + + if (!approved) { + this.console.error('Failed to approve BZZ spending') + process.exit(1) } this.console.log(`Creating postage batch for ${wallet.address} with depth ${this.depth} and amount ${this.amount}`) diff --git a/src/utils/bzz-transaction-utils.ts b/src/utils/bzz-transaction-utils.ts new file mode 100644 index 00000000..b814e71d --- /dev/null +++ b/src/utils/bzz-transaction-utils.ts @@ -0,0 +1,151 @@ +import { Utils } from '@ethersphere/bee-js' +import { BigNumber, Contract, providers, Wallet, utils as ethersUtils } from 'ethers' +import { NETWORK_ID, Contracts, ABI } from './contracts' +import { eth_getBalance, makeReadySigner } from './rpc' +import { CommandLog } from '../command/root-command/command-log' + +/** + * Checks if a wallet has sufficient BZZ funds for an operation + * @param walletAddress The wallet address to check + * @param requiredAmount The required amount in BZZ + * @param availableAmount The available amount in BZZ + * @param console Console instance for output + * @returns True if sufficient funds, false otherwise + */ +export async function checkBzzBalance( + walletAddress: string, + requiredAmount: bigint, + availableAmount: bigint, + console: CommandLog, +): Promise { + // Convert to string for comparison + const requiredAmountStr = requiredAmount.toString() + const availableAmountStr = availableAmount.toString() + + if (BigNumber.from(availableAmountStr).lt(BigNumber.from(requiredAmountStr))) { + console.error(`\nWallet address: 0x${walletAddress} has insufficient BZZ funds.`) + // Format amounts for display + const requiredFormatted = ethersUtils.formatUnits(requiredAmount, 18) + const availableFormatted = ethersUtils.formatUnits(availableAmount, 18) + + console.error(`Required: ${requiredFormatted} BZZ`) + console.error(`Available: ${availableFormatted} BZZ`) + return false + } + return true +} + +/** + * Checks if a wallet has sufficient xDAI funds for gas + * @param walletAddress The wallet address to check + * @param estimatedGasCost The estimated gas cost + * @param console Console instance for output + * @returns True if sufficient funds, false otherwise + */ +export async function checkXdaiBalance( + walletAddress: string, + estimatedGasCost: BigNumber, + console: CommandLog, +): Promise { + const jsonRpcUrl = 'https://xdai.fairdatasociety.org' + const provider = new providers.JsonRpcProvider(jsonRpcUrl, NETWORK_ID) + const xDAI = await eth_getBalance(walletAddress, provider) + const xDAIValue = BigNumber.from(xDAI) + + if (xDAIValue.lt(estimatedGasCost)) { + console.error(`\nWallet address: 0x${walletAddress} has insufficient xDAI funds for gas fees.`) + console.error( + `Required: ~${ethersUtils.formatEther(estimatedGasCost)} xDAI, Available: ${ethersUtils.formatEther( + xDAIValue + )} xDAI`, + ) + return false + } + return true +} + +/** + * Calculates and displays operation costs + * @param depth The depth of the batch + * @param amount The amount in PLUR + * @param bzzBalance The current BZZ balance (optional) + * @param console Console instance for output + * @returns An object containing cost information + */ +export async function calculateAndDisplayCosts( + depth: number, + amount: bigint, + bzzBalance: bigint, + console: CommandLog, +): Promise<{ + bzzCost: any // Keep as 'any' since it's a Utils.getStampCost return type + estimatedGasCost: BigNumber + provider: providers.JsonRpcProvider +}> { + const bzzCost = Utils.getStampCost(depth, amount) + const jsonRpcUrl = 'https://xdai.fairdatasociety.org' + const provider = new providers.JsonRpcProvider(jsonRpcUrl, NETWORK_ID) + // Estimate gas costs + const gasPrice = await provider.getGasPrice() + const gasLimit = BigNumber.from(1000000) // Conservative estimate + const estimatedGasCost = gasPrice.mul(gasLimit) + + console.log(`Operation will cost ${bzzCost.toDecimalString()} BZZ and ~${ethersUtils.formatEther(estimatedGasCost)} xDAI`) + console.log(`Your current balance is ${ethersUtils.formatUnits(bzzBalance, 18)} BZZ`) + + return { bzzCost, estimatedGasCost, provider } +} + +/** + * Checks if the current allowance is sufficient and approves if needed + * @param privateKey The private key of the wallet + * @param requiredAmount The required amount in BZZ (as a string) + * @param console Console instance for output + * @returns True if approval was successful or not needed + */ +export async function checkAndApproveAllowance( + privateKey: string, + requiredAmount: string, + console: CommandLog, +): Promise { + const jsonRpcUrl = 'https://xdai.fairdatasociety.org' + const wallet = new Wallet(privateKey) + const signer = await makeReadySigner(wallet.privateKey, jsonRpcUrl) + + // Check current allowance + const allowanceAbi = [ + { + type: 'function', + stateMutability: 'view', + payable: false, + outputs: [{ type: 'uint256', name: 'remaining' }], + name: 'allowance', + inputs: [ + { type: 'address', name: '_owner' }, + { type: 'address', name: '_spender' }, + ], + constant: true, + }, + ] + + const bzzAllowanceContract = new Contract(Contracts.bzz, allowanceAbi, signer) + const currentAllowance = await bzzAllowanceContract.allowance(wallet.address, Contracts.postageStamp) + console.log(`Current allowance: ${Number(currentAllowance) / 10 ** 18} BZZ`) + + if (currentAllowance.lt(requiredAmount)) { + console.log(`Approving spending of ${requiredAmount} PLUR to ${Contracts.postageStamp}`) + const tokenProxyContract = new Contract(Contracts.bzz, ABI.tokenProxy, signer) + const approve = await tokenProxyContract.approve(Contracts.postageStamp, requiredAmount, { + gasLimit: 130_000, + type: 2, + maxFeePerGas: BigNumber.from(2000000000), // 2 gwei + maxPriorityFeePerGas: BigNumber.from(1000000000), // 1 gwei + }) + console.log(`Waiting 3 blocks on approval tx ${approve.hash}`) + await approve.wait(3) + return true + } else { + console.log(`Approval not needed. Current allowance: ${Number(currentAllowance) / 10 ** 18} BZZ`) + return true + } +} From 0d90844636a6296cdc593bd778199fd2f7c02799 Mon Sep 17 00:00:00 2001 From: Pablo Castellano Date: Sat, 26 Apr 2025 17:12:44 +0200 Subject: [PATCH 3/3] fix: bzz decimals --- src/utils/bzz-transaction-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/bzz-transaction-utils.ts b/src/utils/bzz-transaction-utils.ts index b814e71d..624c014a 100644 --- a/src/utils/bzz-transaction-utils.ts +++ b/src/utils/bzz-transaction-utils.ts @@ -91,7 +91,7 @@ export async function calculateAndDisplayCosts( const estimatedGasCost = gasPrice.mul(gasLimit) console.log(`Operation will cost ${bzzCost.toDecimalString()} BZZ and ~${ethersUtils.formatEther(estimatedGasCost)} xDAI`) - console.log(`Your current balance is ${ethersUtils.formatUnits(bzzBalance, 18)} BZZ`) + console.log(`Your current balance is ${ethersUtils.formatUnits(bzzBalance, 16)} BZZ`) return { bzzCost, estimatedGasCost, provider } }