Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/command/stamp/topup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { stampProperties } from '../../utils/option'
import { createSpinner } from '../../utils/spinner'
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'
Expand All @@ -29,6 +30,66 @@ 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)

// Get wallet address
const { ethereum } = await this.bee.getNodeAddresses()
const walletAddress = ethereum.toHex()

this.console.log(`Topping up stamp ${this.stamp} of depth ${stamp.depth} with ${this.amount} PLUR.\n`)

// Calculate costs
const { bzzCost, estimatedGasCost } = await calculateAndDisplayCosts(
stamp.depth,
this.amount,
bzzBalance.toPLURBigInt(),
this.console
)

this.console.log(`Current price: ${currentPrice.toString()} PLUR per block`)
this.console.log(`Estimated TTL extension: ~${additionalDaysNumber.toFixed(2)} days`)

// Check BZZ balance
const hasSufficientBzz = await checkBzzBalance(
walletAddress,
bzzCost.toPLURBigInt(),
bzzBalance.toPLURBigInt(),
this.console
)

if (!hasSufficientBzz) {
process.exit(1)
}

// Check xDAI balance
const hasSufficientXdai = await checkXdaiBalance(
walletAddress,
estimatedGasCost,
this.console,
)

if (!hasSufficientXdai) {
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) {
Expand Down
69 changes: 53 additions & 16 deletions src/command/utility/create-batch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Utils } from '@ethersphere/bee-js'
import { Numbers, Strings } from 'cafe-utility'
import { Contract, Event, Wallet } from 'ethers'
import { BigNumber, Contract, Event, Wallet } from 'ethers'
import { LeafCommand, Option } from 'furious-commander'
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'
Expand Down Expand Up @@ -49,6 +49,45 @@ export class CreateBatch extends RootCommand implements LeafCommand {
public async run(): Promise<void> {
super.init()

const wallet = new Wallet(this.privateKey)
const signer = await makeReadySigner(wallet.privateKey, this.jsonRpcUrl)

// 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
)

// Check BZZ balance
const hasSufficientBzz = await checkBzzBalance(
wallet.address,
bzzCost.toPLURBigInt(),
bzzBalance.toBigInt(),
this.console
)

if (!hasSufficientBzz) {
process.exit(1)
}

// Check xDAI balance
const hasSufficientXdai = await checkXdaiBalance(
wallet.address,
estimatedGasCost,
this.console
)

if (!hasSufficientXdai) {
process.exit(1)
}

if (!this.yes) {
this.yes = await this.console.confirm(
'This command creates an external batch for advanced usage. Do you want to continue?',
Expand All @@ -59,20 +98,18 @@ 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)

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)
// 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}`)
const postageStampContract = new Contract(Contracts.postageStamp, ABI.postageStamp, signer)
Expand Down
151 changes: 151 additions & 0 deletions src/utils/bzz-transaction-utils.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
// 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<boolean> {
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, 16)} 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<boolean> {
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
}
}