Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2390ddb
chore: upgrade ethers to v6
Cafe137 May 12, 2026
4b31996
refactor: avoid bypassing null errors
Cafe137 May 12, 2026
5684b20
test: add create-batch test
Cafe137 May 12, 2026
e64f166
test: use custom matcher
Cafe137 May 12, 2026
f588b2b
chore: add step to print docker ps
Cafe137 May 12, 2026
976e5c4
test: update JSON-RPC URL in create-batch command test
Cafe137 May 12, 2026
7299325
test: fix JSON-RPC URL in create-batch command test
Cafe137 May 12, 2026
4f8a013
test: update JSON-RPC URL to use HTTP in create-batch command test
Cafe137 May 12, 2026
7f81e71
test: set SWARM_CLI_NETWORK_ID for create-batch command test
Cafe137 May 12, 2026
223ad3f
refactor: use SWARM_CLI_NETWORK_ID from environment variable in NETWO…
Cafe137 May 12, 2026
c897cc2
refactor: replace NETWORK_ID constant with getNetworkId function for …
Cafe137 May 12, 2026
66a6d93
fix: handle missing postage stamp log in create batch transaction rec…
Cafe137 May 12, 2026
0f429ab
refactor: update Contracts to use environment variables for addresses
Cafe137 May 12, 2026
ab8de53
test: try to debug
Cafe137 May 13, 2026
356fa79
test: use correct private key
Cafe137 May 13, 2026
afd0c86
fix: correct amount in create postage batch log
Cafe137 May 13, 2026
9a24bca
test: add redeem test
Cafe137 May 13, 2026
27d1a23
chore: format code
Cafe137 May 13, 2026
1fe15c6
fix: ensure 'to' address starts with '0x' in transaction functions
Cafe137 May 13, 2026
1cf7fe3
chore: remove unnecessary docker ps command from CI workflow
Cafe137 May 13, 2026
06c026b
fix: handle null gas price and transaction receipt in RPC functions
Cafe137 May 13, 2026
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
927 changes: 103 additions & 824 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"chalk": "^2.4.2",
"cli-progress": "^3.11.2",
"cli-table3": "^0.6.5",
"ethers": "^5.7.2",
"ethers": "^6.16.0",
"furious-commander": "^1.7.1",
"inquirer": "^8.2.5",
"node-fetch": "^2.7.0",
Expand Down
2 changes: 1 addition & 1 deletion src/command/stake/recover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class Recover extends RootCommand implements LeafCommand {
]

const wallet = await createWallet(this.walletSource, this.console)
const signer = await makeReadySigner(wallet.getPrivateKeyString(), this.jsonRpcUrl)
const { signer } = await makeReadySigner(wallet.getPrivateKeyString(), this.jsonRpcUrl)
const contract = new Contract(address, abi, signer)

const isPaused = await contract.paused()
Expand Down
7 changes: 3 additions & 4 deletions src/command/stamp/buy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BatchId, Utils } from '@ethersphere/bee-js'
import { Dates, Numbers } from 'cafe-utility'
import chalk from 'chalk'
import { BigNumber } from 'ethers'
import { LeafCommand, Option } from 'furious-commander'
import { exit } from 'process'
import { isChainStateReady } from '../../utils/chainsync'
Expand Down Expand Up @@ -71,10 +70,10 @@ export class Buy extends StampCommand implements LeafCommand {
}

const chainState = await this.bee.getChainState()
const minimumAmount = BigNumber.from(chainState.currentPrice).mul(17280)
const minimumAmount = BigInt(chainState.currentPrice) * 17280n

if (minimumAmount.gte(BigNumber.from(this.amount))) {
this.console.error('The amount is too low. The minimum amount is', minimumAmount.add(1).toString())
if (minimumAmount >= this.amount) {
this.console.error('The amount is too low. The minimum amount is', (minimumAmount + 1n).toString())

return
}
Expand Down
34 changes: 16 additions & 18 deletions src/command/utility/create-batch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Utils } from '@ethersphere/bee-js'
import { Numbers, Strings } from 'cafe-utility'
import { Contract, Event, Wallet } from 'ethers'
import { Contract, ContractTransactionReceipt, Wallet } from 'ethers'
import { LeafCommand, Option } from 'furious-commander'
import { ABI, Contracts } from '../../utils/contracts'
import { makeReadySigner } from '../../utils/rpc'
Expand Down Expand Up @@ -61,7 +61,7 @@ export class CreateBatch extends RootCommand implements LeafCommand {

const wallet = new Wallet(this.privateKey)
const cost = Utils.getStampCost(this.depth, this.amount)
const signer = await makeReadySigner(wallet.privateKey, this.jsonRpcUrl)
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)
Expand All @@ -76,24 +76,22 @@ export class CreateBatch extends RootCommand implements LeafCommand {

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)
const createBatch = await postageStampContract.createBatch(
signer.address,
this.amount,
this.depth,
16,
`0x${Strings.randomHex(64)}`,
false,
{
gasLimit: 1_000_000,
type: 2,
maxFeePerGas: Numbers.make('3gwei'),
maxPriorityFeePerGas: Numbers.make('2gwei'),
},
)
const createBatchArgs = [signer.address, this.amount, this.depth, 16, `0x${Strings.randomHex(64)}`, false]
const createBatch = await postageStampContract.createBatch(...createBatchArgs, {
gasLimit: 1_000_000,
type: 2,
maxFeePerGas: Numbers.make('3gwei'),
maxPriorityFeePerGas: Numbers.make('2gwei'),
})
this.console.log(`Waiting 3 blocks on create batch tx ${createBatch.hash}`)
const receipt = await createBatch.wait(3)
const receipt = (await createBatch.wait(3)) as ContractTransactionReceipt

const batchId = receipt.events.find((x: Event) => x.address === Contracts.postageStamp).topics[1]
const batchLog = receipt.logs.find(x => x.address === Contracts.postageStamp)

if (!batchLog || batchLog.topics.length < 2) {
throw new Error(`Could not find postage stamp log in receipt. Logs: ${JSON.stringify(receipt.logs)}`)
}
const batchId = batchLog.topics[1]
this.console.log(`Batch created with ID ${batchId}`)
}
}
12 changes: 6 additions & 6 deletions src/command/utility/redeem.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Dates, System } from 'cafe-utility'
import { BigNumber, providers, Wallet } from 'ethers'
import { JsonRpcProvider, Wallet } from 'ethers'
import { Argument, LeafCommand, Option } from 'furious-commander'
import { NETWORK_ID } from '../../utils/contracts'
import { getNetworkId } from '../../utils/contracts'
import {
estimateNativeTransferTransactionCost,
eth_getBalance,
Expand Down Expand Up @@ -50,7 +50,7 @@ export class Redeem extends RootCommand implements LeafCommand {
}

this.console.log(`Target wallet address: ${this.target}`)
const provider = new providers.JsonRpcProvider(this.jsonRpcUrl, NETWORK_ID)
const provider = new JsonRpcProvider(this.jsonRpcUrl, getNetworkId())
this.console.log('Creating wallet...')
const wallet = new Wallet(this.wallet, provider)
this.console.log('Fetching xBZZ balance...')
Expand Down Expand Up @@ -92,14 +92,14 @@ export class Redeem extends RootCommand implements LeafCommand {
}
}
const { gasPrice, totalCost } = await estimateNativeTransferTransactionCost(this.wallet, this.jsonRpcUrl)
const xDAIValue = BigNumber.from(xDAI)
const xDAIValue = BigInt(xDAI)

if (xDAIValue.gt(totalCost)) {
if (xDAIValue > totalCost) {
this.console.log('Transferring xDAI to Bee wallet...')
await sendNativeTransaction(
this.wallet,
this.target,
xDAIValue.sub(totalCost).toString(),
(xDAIValue - totalCost).toString(),
this.jsonRpcUrl,
gasPrice,
)
Expand Down
12 changes: 9 additions & 3 deletions src/utils/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
export const NETWORK_ID = 100
export function getNetworkId(): number {
return process.env.SWARM_CLI_NETWORK_ID ? Number(process.env.SWARM_CLI_NETWORK_ID) : 100
}

export const Contracts = {
bzz: '0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da',
postageStamp: '0x45a1502382541Cd610CC9068e88727426b696293',
get bzz() {
return process.env.SWARM_CLI_BZZ_ADDRESS ?? '0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da'
},
get postageStamp() {
return process.env.SWARM_CLI_POSTAGE_STAMP_ADDRESS ?? '0x45a1502382541Cd610CC9068e88727426b696293'
},
}

export const ABI = {
Expand Down
76 changes: 51 additions & 25 deletions src/utils/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { BigNumber as BN, Contract, providers, Wallet } from 'ethers'
import { ABI, Contracts } from './contracts'
import { Contract, JsonRpcProvider, TransactionReceipt, TransactionResponse, Wallet } from 'ethers'
import { ABI, Contracts, getNetworkId } from './contracts'

const NETWORK_ID = 100

export async function eth_getBalance(address: string, provider: providers.JsonRpcProvider): Promise<string> {
export async function eth_getBalance(address: string, provider: JsonRpcProvider): Promise<string> {
if (!address.startsWith('0x')) {
address = `0x${address}`
}
Expand All @@ -14,7 +12,7 @@ export async function eth_getBalance(address: string, provider: providers.JsonRp

export async function eth_getBalanceERC20(
address: string,
provider: providers.JsonRpcProvider,
provider: JsonRpcProvider,
tokenAddress = Contracts.bzz,
): Promise<string> {
if (!address.startsWith('0x')) {
Expand All @@ -27,44 +25,60 @@ export async function eth_getBalanceERC20(
}

interface TransferResponse {
transaction: providers.TransactionResponse
receipt: providers.TransactionReceipt
transaction: TransactionResponse
receipt: TransactionReceipt
}

interface TransferCost {
gasPrice: BN
totalCost: BN
gasPrice: bigint
totalCost: bigint
}

export async function estimateNativeTransferTransactionCost(
privateKey: string,
jsonRpcProvider: string,
): Promise<TransferCost> {
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasLimit = '21000'
const gasPrice = await signer.getGasPrice()
const { provider } = await makeReadySigner(privateKey, jsonRpcProvider)
const gasLimit = 21000n
const { gasPrice } = await provider.getFeeData()

if (gasPrice === null) {
throw new Error('Unable to determine gas price from provider')
}

return { gasPrice, totalCost: gasPrice.mul(gasLimit) }
return { gasPrice, totalCost: gasPrice * gasLimit }
}

export async function sendNativeTransaction(
privateKey: string,
to: string,
value: string,
jsonRpcProvider: string,
externalGasPrice?: BN,
externalGasPrice?: bigint,
): Promise<TransferResponse> {
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasPrice = externalGasPrice ?? (await signer.getGasPrice())
if (!to.startsWith('0x')) {
to = `0x${to}`
}
const { signer, provider } = await makeReadySigner(privateKey, jsonRpcProvider)
const resolvedGasPrice = externalGasPrice ?? (await provider.getFeeData()).gasPrice

if (resolvedGasPrice === null || resolvedGasPrice === undefined) {
throw new Error('Unable to determine gas price from provider')
}

const transaction = await signer.sendTransaction({
to,
value: BN.from(value),
gasPrice,
gasLimit: BN.from(21000),
value: BigInt(value),
gasPrice: resolvedGasPrice,
gasLimit: 21000n,
type: 0,
})
const receipt = await transaction.wait(1)

if (receipt === null) {
throw new Error('Transaction was not included in a block')
}

return { transaction, receipt }
}

Expand All @@ -74,19 +88,31 @@ export async function sendBzzTransaction(
value: string,
jsonRpcProvider: string,
): Promise<TransferResponse> {
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasPrice = await signer.getGasPrice()
if (!to.startsWith('0x')) {
to = `0x${to}`
}
const { signer, provider } = await makeReadySigner(privateKey, jsonRpcProvider)
const { gasPrice } = await provider.getFeeData()

if (gasPrice === null) {
throw new Error('Unable to determine gas price from provider')
}

const bzz = new Contract(Contracts.bzz, ABI.bzz, signer)
const transaction = await bzz.transfer(to, value, { gasPrice })
const receipt = await transaction.wait(1)

if (receipt === null) {
throw new Error('Transaction was not included in a block')
}

return { transaction, receipt }
}

export async function makeReadySigner(privateKey: string, jsonRpcProvider: string) {
const provider = new providers.JsonRpcProvider(jsonRpcProvider, NETWORK_ID)
await provider.ready
const provider = new JsonRpcProvider(jsonRpcProvider, getNetworkId())
await provider.getNetwork()
const signer = new Wallet(privateKey, provider)

return signer
return { signer, provider }
}
34 changes: 34 additions & 0 deletions test/command/create-batch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { toMatchLinesInOrder } from '../custom-matcher'
import { describeCommand, invokeTestCli } from '../utility'

expect.extend({
toMatchLinesInOrder,
})

describeCommand('Test `utility create-batch` command', ({ consoleMessages }) => {
it('should create batch', async () => {
process.env.SWARM_CLI_NETWORK_ID = '4020'
process.env.SWARM_CLI_BZZ_ADDRESS = '0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab'
process.env.SWARM_CLI_POSTAGE_STAMP_ADDRESS = '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B'
await invokeTestCli([
'utility',
'create-batch',
'--private-key',
'0x566058308ad5fa3888173c741a1fb902c9f1f19559b11fc2738dfc53637ce4e9',
'--depth',
'17',
'--amount',
'10B',
'--json-rpc-url',
'http://localhost:9545',
'--yes',
])
expect(consoleMessages).toMatchLinesInOrder([
['Approving spending of', 'BZZ to'],
['Waiting 3 blocks on approval tx'],
['Creating postage batch for', 'with depth 17 and amount 10000000000'],
['Waiting 3 blocks on create batch tx'],
['Batch created with ID'],
])
})
})
48 changes: 48 additions & 0 deletions test/command/redeem.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Wallet } from 'ethers'
import { sendBzzTransaction, sendNativeTransaction } from '../../src/utils/rpc'
import { toMatchLinesInOrder } from '../custom-matcher'
import { describeCommand, invokeTestCli } from '../utility'

expect.extend({
toMatchLinesInOrder,
})

const FUNDER_KEY = '0x566058308ad5fa3888173c741a1fb902c9f1f19559b11fc2738dfc53637ce4e9'
const JSON_RPC_URL = 'http://localhost:9545'
const ETH_01 = 100_000_000_000_000_000n.toString()
const BZZ_5 = 50_000_000_000_000_000n.toString()

describeCommand('Test `utility redeem` command', ({ consoleMessages }) => {
it('should redeem funds to target address', async () => {
process.env.SWARM_CLI_NETWORK_ID = '4020'
process.env.SWARM_CLI_BZZ_ADDRESS = '0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab'

const sourceWallet = Wallet.createRandom()
const targetWallet = Wallet.createRandom()

await sendNativeTransaction(FUNDER_KEY, sourceWallet.address, ETH_01, JSON_RPC_URL)
await sendBzzTransaction(FUNDER_KEY, sourceWallet.address, BZZ_5, JSON_RPC_URL)

await invokeTestCli([
'utility',
'redeem',
sourceWallet.privateKey,
'--json-rpc-url',
JSON_RPC_URL,
'--target',
targetWallet.address,
'--yes',
])

expect(consoleMessages).toMatchLinesInOrder([
['Target wallet address'],
['Creating wallet'],
['xBZZ balance'],
['xDAI balance'],
['Transferring xBZZ'],
['Refreshing xDAI balance'],
['Transferring xDAI'],
['Redeem complete'],
])
}, 60_000)
})
Loading