feat: bee-js revamp (#690)

* chore: initial commit

* refactor: remove unnecessary wrappers

* style: add missing newline

* chore: bump bee-js

* chore: ignore any cast in fdp

* fix: remove cid import

* fix: make TextEncoder and TextDecoder available in jest

* refactor: dedupe stamp ttl second conversion

* refactor: use convenience methods from bee-js

* feat: update to bee-js for restored ens support

* fix: bump bee-js to get download fix

* fix: resolve feed before downloading reference

* fix: fix token displays

* fix: fix token input modal error message

* refactor: remove wallet balance provider

* chore: remove dead code

* refactor: upcoming bee js 0.15.0 (#692)

* chore: initial commit

* fix: do not break page when duration seconds is 0

* ci: remove cache

* chore: upgrade bee-js

* feat: bee-js and bee v2.6 compatibility

* chore: switch upcoming/bee-js to ethersphere/bee-js
This commit is contained in:
Cafe137
2025-07-16 17:10:14 +02:00
committed by GitHub
parent 082a8f52ef
commit 1249c0df71
62 changed files with 675 additions and 16303 deletions
-5
View File
@@ -1,5 +0,0 @@
import axios from 'axios'
export async function requestBzz(address: string): Promise<void> {
await axios.post(`https://xbzz-faucet.apyos.dev/xbzz/${address}`)
}
+5 -6
View File
@@ -1,7 +1,6 @@
import { DAI } from '@ethersphere/bee-js'
import axios from 'axios'
import { BEE_DESKTOP_LATEST_RELEASE_PAGE_API } from '../constants'
import { DaiToken } from '../models/DaiToken'
import { Token } from '../models/Token'
import { getJson, postJson } from './net'
export interface BeeConfig {
@@ -18,10 +17,10 @@ export interface BeeConfig {
'blockchain-rpc-endpoint'?: string
}
export async function getBzzPriceAsDai(desktopUrl: string): Promise<Token> {
export async function getBzzPriceAsDai(desktopUrl: string): Promise<DAI> {
const response = await axios.get(`${desktopUrl}/price`)
return DaiToken.fromDecimal(response.data)
return DAI.fromDecimalString(response.data)
}
export function upgradeToLightNode(desktopUrl: string, rpcProvider: string): Promise<BeeConfig> {
@@ -53,8 +52,8 @@ export async function createGiftWallet(desktopUrl: string, address: string): Pro
await postJson(`${desktopUrl}/gift-wallet/${address}`)
}
export async function performSwap(desktopUrl: string, daiAmount: string): Promise<void> {
await postJson(`${desktopUrl}/swap`, { dai: daiAmount })
export async function performSwap(desktopUrl: string, daiAmount: DAI): Promise<void> {
await postJson(`${desktopUrl}/swap`, { dai: daiAmount.toWeiString() })
}
export async function getLatestBeeDesktopVersion(): Promise<string> {
+7 -7
View File
@@ -1,4 +1,4 @@
import { BatchId, Bee, Reference } from '@ethersphere/bee-js'
import { BatchId, Bee, NULL_TOPIC, Reference } from '@ethersphere/bee-js'
import { Wallet } from 'ethers'
import { uuidV4, waitUntilStampUsable } from '.'
import { Identity, IdentityType } from '../providers/Feeds'
@@ -80,18 +80,18 @@ async function getWallet(type: IdentityType, data: string, password?: string): P
export async function updateFeed(
beeApi: Bee,
identity: Identity,
hash: string,
stamp: string,
hash: Reference | string,
stamp: BatchId | string,
password?: string,
): Promise<void> {
const wallet = await getWalletFromIdentity(identity, password)
if (!identity.feedHash) {
identity.feedHash = (await beeApi.createFeedManifest(stamp, 'sequence', '00'.repeat(32), wallet.address)).reference
identity.feedHash = (await beeApi.createFeedManifest(stamp, NULL_TOPIC, wallet.address)).toHex()
}
const writer = beeApi.makeFeedWriter('sequence', '00'.repeat(32), wallet.privateKey)
const writer = beeApi.makeFeedWriter(NULL_TOPIC, wallet.privateKey)
await waitUntilStampUsable(stamp as BatchId, beeApi)
await writer.upload(stamp, hash as Reference)
await waitUntilStampUsable(stamp, beeApi)
await writer.upload(stamp, hash)
}
+20 -47
View File
@@ -1,8 +1,6 @@
import { BatchId, Bee, PostageBatch } from '@ethersphere/bee-js'
import { decodeCid } from '@ethersphere/swarm-cid'
import { BatchId, Bee, PostageBatch, Reference } from '@ethersphere/bee-js'
import { BigNumber } from 'bignumber.js'
import { BZZ_LINK_DOMAIN } from '../constants'
import { Token } from '../models/Token'
/**
* Test if value is an integer
@@ -131,13 +129,7 @@ export function extractSwarmCid(s: string): string | undefined {
const cid = matches[1]
try {
const decodeResult = decodeCid(cid)
if (!decodeResult.type) {
return
}
return decodeResult.reference
return new Reference(cid).toHex()
} catch (e) {
return
}
@@ -171,48 +163,36 @@ export function formatEnum(string: string): string {
return (string.charAt(0).toUpperCase() + string.slice(1).toLowerCase()).replaceAll('_', ' ')
}
export function secondsToTimeString(seconds: number): string {
export function secondsToTimeString(seconds: number | bigint): string {
seconds = BigInt(seconds)
let unit = seconds
if (unit < 120) {
return `${seconds} seconds`
}
unit /= 60
unit /= BigInt(60)
if (unit < 120) {
return `${Math.round(unit)} minutes`
return `${unit} minutes`
}
unit /= 60
unit /= BigInt(60)
if (unit < 48) {
return `${Math.round(unit)} hours`
return `${unit} hours`
}
unit /= 24
unit /= BigInt(24)
if (unit < 14) {
return `${Math.round(unit)} days`
return `${unit} days`
}
unit /= 7
unit /= BigInt(7)
if (unit < 52) {
return `${Math.round(unit)} weeks`
return `${unit} weeks`
}
unit /= 52
unit /= BigInt(52)
return `${unit.toFixed(1)} years`
}
export function convertAmountToSeconds(amount: number, pricePerBlock: number): number {
// TODO: blocktime should come directly from the blockchain as it may differ between different networks
const blockTime = 5 // On mainnet there is 5 seconds between blocks
// See https://github.com/ethersphere/bee/blob/66f079930d739182c4c79eb6008784afeeba1096/pkg/debugapi/postage.go#L410-L413
return (amount * blockTime) / pricePerBlock
}
export function calculateStampPrice(depth: number, amount: bigint): Token {
// See https://github.com/ethersphere/bee/blob/66f079930d739182c4c79eb6008784afeeba1096/pkg/debugapi/postage.go#L410-L413
return new Token(amount * BigInt(2 ** depth)) // FIXME: the 2 ** depth should be performed on bigint already
return `${unit} years`
}
export function shortenText(text: string, length = 20, separator = '[…]'): string {
@@ -231,20 +211,11 @@ interface Options {
timeout?: number
}
export function waitUntilStampUsable(batchId: BatchId, bee: Bee, options?: Options): Promise<PostageBatch> {
return waitForStamp(batchId, bee, 'usable', options)
export function waitUntilStampUsable(batchId: BatchId | string, bee: Bee, options?: Options): Promise<PostageBatch> {
return waitForStamp(batchId, bee, options)
}
export function waitUntilStampExists(batchId: BatchId, bee: Bee, options?: Options): Promise<PostageBatch> {
return waitForStamp(batchId, bee, 'exists', options)
}
async function waitForStamp(
batchId: BatchId,
bee: Bee,
field: 'exists' | 'usable',
options?: Options,
): Promise<PostageBatch> {
async function waitForStamp(batchId: BatchId | string, bee: Bee, options?: Options): Promise<PostageBatch> {
const timeout = options?.timeout || DEFAULT_STAMP_USABLE_TIMEOUT
const pollingFrequency = options?.pollingFrequency || DEFAULT_POLLING_FREQUENCY
@@ -252,7 +223,9 @@ async function waitForStamp(
try {
const stamp = await bee.getPostageBatch(batchId)
if (stamp[field]) return stamp
if (stamp.usable) {
return stamp
}
} catch {
// ignore
}
-155
View File
@@ -1,155 +0,0 @@
import { Bee, Utils } from '@ethersphere/bee-js'
import { MantarayNode, MetadataMapping, Reference, loadAllNodes } from 'mantaray-js'
interface ValueNode extends MantarayNode {
getEntry: Reference
}
/**
* The ASCII code of the character `/`.
*
* This prefix of the root node holds metadata such as the index document.
*/
const INDEX_DOCUMENT_FORK_PREFIX = '47'
export class ManifestJs {
private bee: Bee
constructor(bee: Bee) {
this.bee = bee
}
/**
* Tests whether a given Swarm hash is a valid mantaray manifest
*/
public async isManifest(hash: string): Promise<boolean> {
try {
const data = await this.bee.downloadData(hash)
const node = new MantarayNode()
node.deserialize(data)
return true
} catch {
return false
}
}
/**
* Retrieves `website-index-document` from a Swarm hash, or `null` if it is not present
*/
public async getIndexDocumentPath(hash: string): Promise<string | null> {
const metadata = await this.getRootSlashMetadata(hash)
if (!metadata) {
return null
}
return metadata['website-index-document'] || null
}
/**
* Retrieves all paths with the associated hashes from a Swarm manifest
*/
public async getHashes(hash: string, options?: { exclude: string[] }): Promise<Record<string, string>> {
const data = await this.bee.downloadData(hash)
const node = new MantarayNode()
node.deserialize(data)
await loadAllNodes(this.load.bind(this), node)
const result: Record<string, string> = {}
this.extractHashes(result, node)
if (options?.exclude) {
for (const path of options.exclude) {
delete result[path]
}
}
return result
}
/**
* Resolves an arbitrary Swarm feed manifest to its latest update reference.
* @returns `/bzz` root manifest hash, or `Promise<null>` if hash is not a feed manifest
* @throws in case of network errors or bad input
*/
public async resolveFeedManifest(hash: string): Promise<string | null> {
const metadata = await this.getRootSlashMetadata(hash)
if (!metadata) {
return null
}
const owner = metadata['swarm-feed-owner']
const topic = metadata['swarm-feed-topic']
if (!owner || !topic) {
return null
}
const reader = this.bee.makeFeedReader('sequence', topic, owner)
const response = await reader.download()
return response.reference
}
private async getRootSlashMetadata(hash: string): Promise<MetadataMapping | null> {
const data = await this.bee.downloadData(hash)
const node = new MantarayNode()
node.deserialize(data)
if (!node.forks) {
return null
}
const fork = node.forks[INDEX_DOCUMENT_FORK_PREFIX]
if (!fork) {
return null
}
const metadataNode = fork.node
if (!metadataNode.IsWithMetadataType()) {
return null
}
const metadata = metadataNode.getMetadata
if (!metadata) {
return null
}
return metadata
}
private extractHashes(result: Record<string, string>, node: MantarayNode, prefix = ''): void {
if (!node.forks) {
return
}
for (const fork of Object.values(node.forks)) {
const path = prefix + this.bytesToUtf8(fork.prefix)
const childNode = fork.node
if (this.isValueNode(childNode, path)) {
result[path] = Utils.bytesToHex(childNode.getEntry)
}
if (childNode.isEdgeType()) {
this.extractHashes(result, childNode, path)
}
}
}
private load(reference: Uint8Array) {
return this.bee.downloadData(Utils.bytesToHex(reference))
}
private bytesToUtf8(bytes: Uint8Array): string {
return new TextDecoder('utf-8').decode(bytes)
}
private isValueNode(node: MantarayNode, path: string): node is ValueNode {
return !this.isRootSlash(node, path) && node.isValueType() && typeof node.getEntry !== 'undefined'
}
private isRootSlash(node: MantarayNode, path: string): boolean {
return path === '/' && node.IsWithMetadataType()
}
}
+39 -32
View File
@@ -1,6 +1,7 @@
import { debounce } from '@material-ui/core'
import { Contract, providers, Wallet, BigNumber as BN } from 'ethers'
import { bzzABI, BZZ_TOKEN_ADDRESS } from './bzz-abi'
import { BZZ, DAI, EthAddress, PrivateKey } from '@ethersphere/bee-js'
import { BigNumber as BN, Contract, providers, Wallet } from 'ethers'
import { BZZ_TOKEN_ADDRESS, bzzABI } from './bzz-abi'
const NETWORK_ID = 100
@@ -12,13 +13,12 @@ async function getNetworkChainId(url: string): Promise<number> {
return network.chainId
}
async function eth_getBalance(address: string, provider: providers.JsonRpcProvider): Promise<string> {
if (!address.startsWith('0x')) {
address = `0x${address}`
}
const balance = await provider.getBalance(address)
async function eth_getBalance(address: EthAddress | string, provider: providers.JsonRpcProvider): Promise<DAI> {
address = new EthAddress(address)
return balance.toString()
const balance = await provider.getBalance(address.toHex())
return DAI.fromWei(balance.toString())
}
async function eth_getBlockByNumber(provider: providers.JsonRpcProvider): Promise<string> {
@@ -28,17 +28,16 @@ async function eth_getBlockByNumber(provider: providers.JsonRpcProvider): Promis
}
async function eth_getBalanceERC20(
address: string,
address: EthAddress | string,
provider: providers.JsonRpcProvider,
tokenAddress = BZZ_TOKEN_ADDRESS,
): Promise<string> {
if (!address.startsWith('0x')) {
address = `0x${address}`
}
const contract = new Contract(tokenAddress, bzzABI, provider)
const balance = await contract.balanceOf(address)
): Promise<BZZ> {
address = new EthAddress(address)
return balance.toString()
const contract = new Contract(tokenAddress, bzzABI, provider)
const balance = await contract.balanceOf(address.toHex())
return BZZ.fromPLUR(balance.toString())
}
interface TransferResponse {
@@ -47,29 +46,34 @@ interface TransferResponse {
}
export async function estimateNativeTransferTransactionCost(
privateKey: string,
privateKey: PrivateKey | string,
jsonRpcProvider: string,
): Promise<{ gasPrice: BN; totalCost: BN }> {
): Promise<{ gasPrice: DAI; totalCost: DAI }> {
privateKey = new PrivateKey(privateKey)
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasLimit = '21000'
const gasPrice = await signer.getGasPrice()
return { gasPrice, totalCost: gasPrice.mul(gasLimit) }
return { gasPrice: DAI.fromWei(gasPrice.toString()), totalCost: DAI.fromWei(gasPrice.mul(gasLimit).toString()) }
}
export async function sendNativeTransaction(
privateKey: string,
to: string,
value: string,
privateKey: PrivateKey | string,
to: EthAddress | string,
value: DAI,
jsonRpcProvider: string,
externalGasPrice?: BN,
externalGasPrice?: DAI,
): Promise<TransferResponse> {
privateKey = new PrivateKey(privateKey)
to = new EthAddress(to)
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasPrice = externalGasPrice ?? (await signer.getGasPrice())
const gasPrice = externalGasPrice ?? DAI.fromWei((await signer.getGasPrice()).toString())
const transaction = await signer.sendTransaction({
to,
value: BN.from(value),
gasPrice,
to: to.toHex(),
value: BN.from(value.toWeiString()),
gasPrice: BN.from(gasPrice.toWeiString()),
gasLimit: BN.from(21000),
type: 0,
})
@@ -79,11 +83,14 @@ export async function sendNativeTransaction(
}
export async function sendBzzTransaction(
privateKey: string,
to: string,
value: string,
privateKey: PrivateKey | string,
to: EthAddress | string,
value: BZZ,
jsonRpcProvider: string,
): Promise<TransferResponse> {
privateKey = new PrivateKey(privateKey)
to = new EthAddress(to)
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasPrice = await signer.getGasPrice()
const bzz = new Contract(BZZ_TOKEN_ADDRESS, bzzABI, signer)
@@ -93,10 +100,10 @@ export async function sendBzzTransaction(
return { transaction, receipt }
}
async function makeReadySigner(privateKey: string, jsonRpcProvider: string) {
async function makeReadySigner(privateKey: PrivateKey, jsonRpcProvider: string) {
const provider = new providers.JsonRpcProvider(jsonRpcProvider, NETWORK_ID)
await provider.ready
const signer = new Wallet(privateKey, provider)
const signer = new Wallet(privateKey.toUint8Array(), provider)
return signer
}
+12 -36
View File
@@ -1,39 +1,15 @@
import { BZZ, DAI, EthAddress } from '@ethersphere/bee-js'
import { providers, Wallet } from 'ethers'
import { BzzToken } from '../models/BzzToken'
import { DaiToken } from '../models/DaiToken'
import { estimateNativeTransferTransactionCost, Rpc } from './rpc'
export class WalletAddress {
private constructor(
public address: string,
public bzz: BzzToken,
public dai: DaiToken,
public provider: providers.JsonRpcProvider,
) {}
static async make(address: string, provider: providers.JsonRpcProvider): Promise<WalletAddress> {
const bzz = new BzzToken(await Rpc._eth_getBalanceERC20(address, provider))
const dai = new DaiToken(await Rpc._eth_getBalance(address, provider))
return new WalletAddress(address, bzz, dai, provider)
}
public async refresh(): Promise<WalletAddress> {
this.bzz = new BzzToken(await Rpc._eth_getBalanceERC20(this.address, this.provider))
this.dai = new DaiToken(await Rpc._eth_getBalance(this.address, this.provider))
return this
}
}
export class ResolvedWallet {
public address: string
public privateKey: string
private constructor(
public wallet: Wallet,
public bzz: BzzToken,
public dai: DaiToken,
public bzz: BZZ,
public dai: DAI,
public provider: providers.JsonRpcProvider,
) {
this.address = wallet.address
@@ -44,32 +20,32 @@ export class ResolvedWallet {
const wallet =
typeof privateKeyOrWallet === 'string' ? new Wallet(privateKeyOrWallet, provider) : privateKeyOrWallet
const address = wallet.address
const bzz = new BzzToken(await Rpc._eth_getBalanceERC20(address, provider))
const dai = new DaiToken(await Rpc._eth_getBalance(address, provider))
const bzz = await Rpc._eth_getBalanceERC20(address, provider)
const dai = await Rpc._eth_getBalance(address, provider)
return new ResolvedWallet(wallet, bzz, dai, provider)
}
public async refresh(): Promise<ResolvedWallet> {
this.bzz = new BzzToken(await Rpc._eth_getBalanceERC20(this.address, this.provider))
this.dai = new DaiToken(await Rpc._eth_getBalance(this.address, this.provider))
this.bzz = await Rpc._eth_getBalanceERC20(this.address, this.provider)
this.dai = await Rpc._eth_getBalance(this.address, this.provider)
return this
}
public async transfer(destination: string, jsonRpcProvider: string): Promise<void> {
if (this.bzz.toDecimal.gt(0.05)) {
await Rpc.sendBzzTransaction(this.privateKey, destination, this.bzz.toString, jsonRpcProvider)
public async transfer(destination: EthAddress | string, jsonRpcProvider: string): Promise<void> {
if (this.bzz.gt(BZZ.fromDecimalString('0.05'))) {
await Rpc.sendBzzTransaction(this.privateKey, destination, this.bzz, jsonRpcProvider)
await this.refresh()
}
const { gasPrice, totalCost } = await estimateNativeTransferTransactionCost(this.privateKey, jsonRpcProvider)
if (this.dai.toBigNumber.gt(totalCost.toString())) {
if (this.dai.gt(totalCost)) {
await Rpc.sendNativeTransaction(
this.privateKey,
destination,
this.dai.toBigNumber.minus(totalCost.toString()).toString(),
this.dai.minus(totalCost),
jsonRpcProvider,
gasPrice,
)