feat: sync and update with all changes from fork (#720)

* feat: sync and update with all changes from fork
* refactor: extract clipboard copy logic into custom hook
* fix: correct spelling of DEFAULT_REFRESH_FREQUENCY_MS in Stamps and WalletBalance providers
* refactor(ui-tests): replace fixed sleeps with condition-based waits
* fix: handle null values for size and granteeCount in infoGroups
* fix(lint): add newline at end of file in useClipboardCopy hook
* fix(ui-tests): page.goto URL
* refactor: update import paths for useClipboardCopy

---------

Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
This commit is contained in:
Bálint Ujvári
2026-03-02 11:34:39 +01:00
committed by GitHub
parent b0f00a624a
commit 519c411db0
303 changed files with 16609 additions and 29415 deletions
-10
View File
@@ -1,10 +0,0 @@
export class AuthError extends Error {
constructor() {
super('Bad API key')
this.name = 'AuthError'
}
}
export function isAuthError(error: unknown): error is AuthError {
return error instanceof Error && error.name === 'AuthError'
}
+15 -1
View File
@@ -1,3 +1,9 @@
import { EthAddress } from '@ethersphere/bee-js'
import { getAddress, JsonRpcProvider, Networkish } from 'ethers'
export const GNOIS_NETWORK_ID = 100
export const GnosisNetwork: Networkish = { chainId: GNOIS_NETWORK_ID, name: 'gnosis', ensAddress: undefined }
const chains = [
{
name: 'Ethereum Mainnet',
@@ -21,10 +27,18 @@ const chains = [
},
{
name: 'Gnosis Chain',
chainId: 100,
chainId: GNOIS_NETWORK_ID,
},
]
export function chainIdToName(chainId: number): string {
return chains.find(record => record.chainId === chainId)?.name || 'Unknown'
}
export function ethAddressString(address: EthAddress | string): string {
return typeof address === 'string' ? getAddress(address) : getAddress(address.toHex())
}
export function newGnosisProvider(url: string): JsonRpcProvider {
return new JsonRpcProvider(url, GnosisNetwork, { staticNetwork: true })
}
+2
View File
@@ -1,6 +1,8 @@
import { DAI } from '@ethersphere/bee-js'
import axios from 'axios'
import { BEE_DESKTOP_LATEST_RELEASE_PAGE_API } from '../constants'
import { getJson, postJson } from './net'
export interface BeeConfig {
+10 -1
View File
@@ -1,4 +1,13 @@
import { isAuthError } from './AuthError'
export class AuthError extends Error {
constructor() {
super('Bad API key')
this.name = 'AuthError'
}
}
export function isAuthError(error: unknown): error is AuthError {
return error instanceof Error && error.name === 'AuthError'
}
export class SwapError extends Error {
snackbarMessage: string
+2 -1
View File
@@ -1,6 +1,6 @@
import { isSupportedAudioType } from './audio'
import { isSupportedImageType } from './image'
import { isSupportedVideoType } from './video'
import { isSupportedAudioType } from './audio'
const indexHtmls = ['index.html', 'index.htm']
@@ -134,5 +134,6 @@ export function packageFile(file: FilePath, pathOverwrite?: string): FilePath {
slice: (start: number, end: number) => file.slice(start, end),
text: file.text,
arrayBuffer: async () => await file.arrayBuffer(),
bytes: file.bytes,
}
}
+32 -16
View File
@@ -1,10 +1,15 @@
import { BatchId, Bee, NULL_TOPIC, Reference } from '@ethersphere/bee-js'
import { Wallet } from 'ethers'
import { uuidV4, waitUntilStampUsable } from '.'
import { BatchId, Bee, NULL_TOPIC, PrivateKey, Reference } from '@ethersphere/bee-js'
import { randomBytes, Wallet } from 'ethers'
import { Identity, IdentityType } from '../providers/Feeds'
import { LocalStorageKeys } from './localStorage'
import { uuidV4, waitUntilStampUsable } from '.'
export function generateWallet(): Wallet {
return Wallet.createRandom()
const privateKey = randomBytes(PrivateKey.LENGTH).toString()
return new Wallet(privateKey)
}
export function persistIdentity(identities: Identity[], identity: Identity): void {
@@ -14,11 +19,11 @@ export function persistIdentity(identities: Identity[], identity: Identity): voi
identities.splice(existingIndex, 1)
}
identities.unshift(identity)
localStorage.setItem('feeds', JSON.stringify(identities))
localStorage.setItem(LocalStorageKeys.feeds, JSON.stringify(identities))
}
export function persistIdentitiesWithoutUpdate(identities: Identity[]): void {
localStorage.setItem('feeds', JSON.stringify(identities))
localStorage.setItem(LocalStorageKeys.feeds, JSON.stringify(identities))
}
export async function convertWalletToIdentity(
@@ -27,16 +32,17 @@ export async function convertWalletToIdentity(
name: string,
password?: string,
): Promise<Identity> {
if (type === 'V3' && !password) {
if (type === IdentityType.V3 && !password) {
throw Error('V3 passwords require password')
}
const identityString = type === 'PRIVATE_KEY' ? identity.privateKey : await identity.encrypt(password as string)
const identityString =
type === IdentityType.PrivateKey ? identity.privateKey : await identity.encrypt(password as string)
return {
uuid: uuidV4(),
name,
type: password ? 'V3' : 'PRIVATE_KEY',
type: password ? IdentityType.V3 : IdentityType.PrivateKey,
address: identity.address,
identity: identityString,
}
@@ -44,26 +50,26 @@ export async function convertWalletToIdentity(
export async function importIdentity(name: string, data: string): Promise<Identity | null> {
if (data.length === 64) {
const wallet = await getWallet('PRIVATE_KEY', data)
const wallet = await getWallet(IdentityType.PrivateKey, data)
return {
uuid: uuidV4(),
name,
type: 'PRIVATE_KEY',
type: IdentityType.PrivateKey,
identity: data,
address: wallet.address,
}
}
if (data.length === 66 && data.toLowerCase().startsWith('0x')) {
const wallet = await getWallet('PRIVATE_KEY', data.slice(2))
const wallet = await getWallet(IdentityType.PrivateKey, data.slice(2))
return { uuid: uuidV4(), name, type: 'PRIVATE_KEY', identity: data, address: wallet.address }
return { uuid: uuidV4(), name, type: IdentityType.PrivateKey, identity: data, address: wallet.address }
}
try {
const { address } = JSON.parse(data)
return { uuid: uuidV4(), name, type: 'V3', identity: data, address }
return { uuid: uuidV4(), name, type: IdentityType.V3, identity: data, address }
} catch {
return null
}
@@ -74,7 +80,17 @@ function getWalletFromIdentity(identity: Identity, password?: string): Promise<W
}
async function getWallet(type: IdentityType, data: string, password?: string): Promise<Wallet> {
return type === 'PRIVATE_KEY' ? new Wallet(data) : await Wallet.fromEncryptedJson(data, password as string)
if (type === IdentityType.PrivateKey) {
return new Wallet(data)
}
if (!password) {
throw new Error('password is required for wallet')
}
const w = await Wallet.fromEncryptedJson(data, password)
return new Wallet(w.privateKey)
}
export async function updateFeed(
@@ -93,5 +109,5 @@ export async function updateFeed(
const writer = beeApi.makeFeedWriter(NULL_TOPIC, wallet.privateKey)
await waitUntilStampUsable(stamp, beeApi)
await writer.upload(stamp, hash)
await writer.uploadReference(stamp, hash)
}
-186
View File
@@ -1,186 +0,0 @@
import { extractSwarmHash, extractSwarmCid, extractEns, recognizeEnsOrSwarmHash } from './index'
interface TestObject {
input: string
expectedOutput: string | undefined
}
const correctHashes: TestObject[] = [
// non-encrypted
{
input: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input: 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input: 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input: 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/',
expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input: 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/',
expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
// encrypted
{
input:
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
expectedOutput:
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input:
'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
expectedOutput:
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input:
'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
expectedOutput:
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input:
'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/',
expectedOutput:
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
{
input:
'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/',
expectedOutput:
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f',
},
]
const wrongHashes: string[] = [
// one character too long
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa',
'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa',
'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa',
'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa/',
'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa/',
// a bit shorter
'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d',
]
describe('extractSwarmHash', () => {
test('should correctly extract hash', () => {
correctHashes.forEach(({ input, expectedOutput }) => {
const hash = extractSwarmHash(input)
expect(hash).toBe(expectedOutput)
})
})
test('should not extract hash from incorrect inputs', () => {
wrongHashes.forEach(input => {
const hash = extractSwarmHash(input)
expect(hash).toBe(undefined)
})
})
})
const correctCids: TestObject[] = [
{
input: 'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.bzz.link',
expectedOutput: 'e80a3df165abbf275ae5480e9e51241d2e6368c4ed379771424af29ca35b29d4',
},
{
input: 'https://bah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vookq.bzz.link',
expectedOutput: 'd2f269c0b99d5bbbcdb93d7f0a85815ad23f851dd2fa94509124c401f7b57395',
},
]
const wrongCids: string[] = [
'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.another.domain',
'http://bah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vookq.bzz.link',
'https://not_cid.bzz.link',
'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.subdomain.bzz.link',
'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.subdomain.bzz.link',
'https://bah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vook.bzz.link',
'https://aah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vookq.bzz.link',
]
describe('extractSwarmCid', () => {
test('should correctly extract hash', () => {
correctCids.forEach(({ input, expectedOutput }) => {
const hash = extractSwarmCid(input)
expect(hash).toBe(expectedOutput)
})
})
test('should not extract cid from incorrect urls', () => {
wrongCids.forEach(url => {
const hash = extractSwarmCid(url)
expect(hash).toBe(undefined)
})
})
})
const correctEns: TestObject[] = [
{
input: 'test.eth',
expectedOutput: 'test.eth',
},
{
input: 't-est.eth',
expectedOutput: 't-est.eth',
},
{
input: 'http://test.eth/whatever',
expectedOutput: 'test.eth',
},
{
input: 'https://alice.test.eth?whatever',
expectedOutput: 'alice.test.eth',
},
{
input: 'swarm.example.eth/?id=1&page=2',
expectedOutput: 'swarm.example.eth',
},
{
input: 'http://swarm.example.eth#up',
expectedOutput: 'swarm.example.eth',
},
{
input: 'http://site.eth:8008',
expectedOutput: 'site.eth',
},
]
const wrongEns: string[] = ['http://test.ethereum/whatever']
describe('extractEns', () => {
test('should correctly extract ens domain', () => {
correctEns.forEach(({ input, expectedOutput }) => {
const hash = extractEns(input)
expect(hash).toBe(expectedOutput)
})
})
test('should not extract ens from incorrect inputs', () => {
wrongEns.forEach(url => {
const hash = extractEns(url)
expect(hash).toBe(undefined)
})
})
})
describe('recognizeEnsOrSwarmHash', () => {
test('should correctly extract hash or ens', () => {
;[...correctHashes, ...correctCids, ...correctEns].forEach(({ input, expectedOutput }) => {
const hash = recognizeEnsOrSwarmHash(input)
expect(hash).toBe(expectedOutput)
})
})
test('should not extract hash or ens from incorrect inputs but instead return them', () => {
;[...wrongHashes, ...wrongCids, ...wrongEns].forEach(url => {
const hash = recognizeEnsOrSwarmHash(url)
expect(hash).toBe(url)
})
})
})
+17 -13
View File
@@ -1,5 +1,6 @@
import { BatchId, Bee, PostageBatch, Reference } from '@ethersphere/bee-js'
import { BigNumber } from 'bignumber.js'
import { BZZ_LINK_DOMAIN } from '../constants'
/**
@@ -90,21 +91,25 @@ export function unwrapPromiseSettlements<T>(
* If all attempts fail, then this `Promise<T>` also rejects.
*/
export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 1000): Promise<T> {
return new Promise(async (resolve, reject) => {
for (let tries = 0; tries < maxRetries; tries++) {
try {
const results = await fn()
resolve(results)
return new Promise((resolve, reject) => {
const attempt = async () => {
for (let tries = 0; tries < maxRetries; tries++) {
try {
const results = await fn()
resolve(results)
return
} catch (error) {
if (tries < maxRetries - 1) {
await sleepMs(delayMs)
} else {
reject(error)
return
} catch (error) {
if (tries < maxRetries - 1) {
await sleepMs(delayMs)
} else {
reject(error)
}
}
}
}
attempt()
})
}
@@ -130,7 +135,7 @@ export function extractSwarmCid(s: string): string | undefined {
const cid = matches[1]
try {
return new Reference(cid).toHex()
} catch (e) {
} catch {
return
}
}
@@ -211,7 +216,6 @@ interface Options {
timeout?: number
}
// TODO: merge this with FM component getUsableStamps()
export function waitUntilStampUsable(batchId: BatchId | string, bee: Bee, options?: Options): Promise<PostageBatch> {
return waitForStamp(batchId, bee, options)
}
@@ -1,9 +1,22 @@
import { shortenHash } from './hash'
export enum HISTORY_KEYS {
UPLOAD_HISTORY = 'UPLOAD_HISTORY',
DOWNLOAD_HISTORY = 'DOWNLOAD_HISTORY',
}
export const LocalStorageKeys = {
providerUrl: 'json-rpc-provider',
apiHost: 'api_host',
apiKey: 'apiKey',
fmClickStorage: 'fm_click_count_v1',
fmSurveyTriggered: 'fm_survey_triggered_v1',
fmSortKey: 'fm.sort.v1',
fmPrivateKey: 'privateKey',
feeds: 'feeds',
depositWallet: 'deposit-wallet',
giftWallets: 'gift-wallets',
invitation: 'invitation',
uploadHistory: 'UPLOAD_HISTORY',
downloadHistory: 'DOWNLOAD_HISTORY',
} as const
export type HISTORY_KEYS = typeof LocalStorageKeys.uploadHistory | typeof LocalStorageKeys.downloadHistory
export interface HistoryItem {
createdAt: number
@@ -11,7 +24,7 @@ export interface HistoryItem {
hash: string
}
export function putHistory(key: string, hash: string, name: string): void {
export function putHistory(key: HISTORY_KEYS, hash: string, name: string): void {
const history = getHistorySafe(key)
const existingIndex = history.findIndex(x => x.hash === hash)
@@ -32,7 +45,7 @@ export function putHistory(key: string, hash: string, name: string): void {
localStorage.setItem(key, JSON.stringify(history))
}
export function getHistorySafe(key: string): HistoryItem[] {
export function getHistorySafe(key: HISTORY_KEYS): HistoryItem[] {
const items = localStorage.getItem(key)
if (!items) {
+21
View File
@@ -0,0 +1,21 @@
import type { Bee, DownloadOptions } from '@ethersphere/bee-js'
import { BeeRequestOptions, MantarayNode, NULL_ADDRESS } from '@ethersphere/bee-js'
export async function loadManifest(
beeApi: Bee,
hash: string,
options?: DownloadOptions,
requestOptions?: BeeRequestOptions,
): Promise<MantarayNode> {
let manifest = await MantarayNode.unmarshal(beeApi, hash, options, requestOptions)
await manifest.loadRecursively(beeApi, options, requestOptions)
// If the manifest is a feed, resolve it and overwrite the manifest
const feed = await manifest.resolveFeed(beeApi, requestOptions)
await feed.ifPresentAsync(async feedUpdate => {
manifest = MantarayNode.unmarshalFromData(feedUpdate.payload.toUint8Array(), NULL_ADDRESS)
await manifest.loadRecursively(beeApi, options, requestOptions)
})
return manifest
}
+7 -4
View File
@@ -1,12 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios from 'axios'
import { AuthError } from './AuthError'
import { AuthError } from './errors'
import { LocalStorageKeys } from './localStorage'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getJson<T extends Record<string, any>>(url: string): Promise<T> {
return sendRequest(url, 'GET') as Promise<T>
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function postJson<T extends Record<string, any>>(url: string, data?: Record<string, any>): Promise<T> {
return sendRequest(url, 'POST', data) as Promise<T>
}
@@ -15,8 +17,9 @@ export async function sendRequest(
url: string,
method: 'GET' | 'POST',
data?: Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<Record<string, any>> {
const authorization = localStorage.getItem('apiKey')
const authorization = localStorage.getItem(LocalStorageKeys.apiKey)
if (!authorization) {
throw Error('API key not found in local storage. Please reopen via Swarm Desktop > Open Web UI.')
+86 -37
View File
@@ -1,27 +1,25 @@
import { debounce } from '@material-ui/core'
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'
import { debounce } from '@mui/material'
import { Contract, JsonRpcProvider, TransactionReceipt, TransactionResponse, Wallet } from 'ethers'
const NETWORK_ID = 100
import { BZZ_TOKEN_ADDRESS, bzzABI } from './bzzAbi'
import { ethAddressString, newGnosisProvider } from './chain'
async function getNetworkChainId(url: string): Promise<number> {
const provider = new providers.JsonRpcProvider(url, NETWORK_ID)
await provider.ready
async function getNetworkChainId(url: string): Promise<bigint> {
const provider = newGnosisProvider(url)
const network = await provider.getNetwork()
return network.chainId
}
async function eth_getBalance(address: EthAddress | string, provider: providers.JsonRpcProvider): Promise<DAI> {
address = new EthAddress(address)
const balance = await provider.getBalance(address.toHex())
async function eth_getBalance(address: EthAddress | string, provider: JsonRpcProvider): Promise<DAI> {
const addressString = ethAddressString(address)
const balance = await provider.getBalance(addressString)
return DAI.fromWei(balance.toString())
}
async function eth_getBlockByNumber(provider: providers.JsonRpcProvider): Promise<string> {
async function eth_getBlockByNumber(provider: JsonRpcProvider): Promise<string> {
const blockNumber = await provider.getBlockNumber()
return blockNumber.toString()
@@ -29,56 +27,75 @@ async function eth_getBlockByNumber(provider: providers.JsonRpcProvider): Promis
async function eth_getBalanceERC20(
address: EthAddress | string,
provider: providers.JsonRpcProvider,
provider: JsonRpcProvider,
tokenAddress = BZZ_TOKEN_ADDRESS,
): Promise<BZZ> {
address = new EthAddress(address)
const addressString = ethAddressString(address)
const contract = new Contract(tokenAddress, bzzABI, provider)
const balance = await contract.balanceOf(address.toHex())
// Use staticCall directly to bypass argument resolution
const balance = await contract.balanceOf.staticCall(addressString)
return BZZ.fromPLUR(balance.toString())
}
interface TransferResponse {
transaction: providers.TransactionResponse
receipt: providers.TransactionReceipt
transaction: TransactionResponse
receipt: TransactionReceipt
}
export async function estimateNativeTransferTransactionCost(
privateKey: PrivateKey | string,
jsonRpcProvider: string,
jsonRpcProviderUrl: string,
): Promise<{ gasPrice: DAI; totalCost: DAI }> {
privateKey = new PrivateKey(privateKey)
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasLimit = '21000'
const gasPrice = await signer.getGasPrice()
const signer = await makeReadySigner(privateKey, jsonRpcProviderUrl)
return { gasPrice: DAI.fromWei(gasPrice.toString()), totalCost: DAI.fromWei(gasPrice.mul(gasLimit).toString()) }
if (!signer.provider) {
throw new Error('Signer provider is invalid!')
}
const gasLimit = BigInt(21000)
const feeData = await signer.provider.getFeeData()
const gasPrice = feeData.gasPrice || BigInt(0)
return {
gasPrice: DAI.fromWei(gasPrice.toString()),
totalCost: DAI.fromWei((gasPrice * gasLimit).toString()),
}
}
export async function sendNativeTransaction(
privateKey: PrivateKey | string,
to: EthAddress | string,
value: DAI,
jsonRpcProvider: string,
jsonRpcProviderUrl: string,
externalGasPrice?: DAI,
): Promise<TransferResponse> {
privateKey = new PrivateKey(privateKey)
to = new EthAddress(to)
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasPrice = externalGasPrice ?? DAI.fromWei((await signer.getGasPrice()).toString())
const signer = await makeReadySigner(privateKey, jsonRpcProviderUrl)
if (!signer.provider) {
throw new Error('Signer provider is invalid!')
}
const feedData = await signer.provider.getFeeData()
const gasPrice = externalGasPrice ?? DAI.fromWei(feedData.gasPrice?.toString() || '0')
const transaction = await signer.sendTransaction({
to: to.toHex(),
value: BN.from(value.toWeiString()),
gasPrice: BN.from(gasPrice.toWeiString()),
gasLimit: BN.from(21000),
value: BigInt(value.toWeiString()),
gasPrice: BigInt(gasPrice.toWeiString()),
gasLimit: BigInt(21000),
type: 0,
})
const receipt = await transaction.wait(1)
if (!receipt) {
throw new Error('Invalid receipt!')
}
return { transaction, receipt }
}
@@ -86,29 +103,61 @@ export async function sendBzzTransaction(
privateKey: PrivateKey | string,
to: EthAddress | string,
value: BZZ,
jsonRpcProvider: string,
jsonRpcProviderUrl: string,
): Promise<TransferResponse> {
privateKey = new PrivateKey(privateKey)
to = new EthAddress(to)
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
const gasPrice = await signer.getGasPrice()
const signer = await makeReadySigner(privateKey, jsonRpcProviderUrl)
if (!signer.provider) {
throw new Error('Signer provider is invalid!')
}
const feeData = await signer.provider.getFeeData()
const gasPrice = feeData.gasPrice || BigInt(0)
const bzz = new Contract(BZZ_TOKEN_ADDRESS, bzzABI, signer)
const transaction = await bzz.transfer(to, value, { gasPrice })
const receipt = await transaction.wait(1)
if (!receipt) {
throw new Error('Invalid receipt!')
}
return { transaction, receipt }
}
async function makeReadySigner(privateKey: PrivateKey, jsonRpcProvider: string) {
const provider = new providers.JsonRpcProvider(jsonRpcProvider, NETWORK_ID)
await provider.ready
const signer = new Wallet(privateKey.toUint8Array(), provider)
async function makeReadySigner(privateKey: PrivateKey, jsonRpcProviderUrl: string) {
const provider = newGnosisProvider(jsonRpcProviderUrl)
await provider.getNetwork()
const signer = new Wallet(privateKey.toString(), provider)
return signer
}
export const Rpc = {
export interface Rpc {
getNetworkChainId: (url: string) => Promise<bigint>
sendNativeTransaction: (
privateKey: PrivateKey | string,
to: EthAddress | string,
value: DAI,
jsonRpcProviderUrl: string,
externalGasPrice?: DAI,
) => Promise<TransferResponse>
sendBzzTransaction: (
privateKey: PrivateKey | string,
to: EthAddress | string,
value: BZZ,
jsonRpcProviderUrl: string,
) => Promise<TransferResponse>
_eth_getBalance: (address: EthAddress | string, provider: JsonRpcProvider) => Promise<DAI>
_eth_getBalanceERC20: (address: EthAddress | string, provider: JsonRpcProvider, tokenAddress?: string) => Promise<BZZ>
eth_getBalance: (address: EthAddress | string, provider: JsonRpcProvider) => Promise<DAI>
eth_getBalanceERC20: (address: EthAddress | string, provider: JsonRpcProvider, tokenAddress: string) => Promise<BZZ>
eth_getBlockByNumber: (provider: JsonRpcProvider) => Promise<string>
}
export const RPC: Rpc = {
getNetworkChainId,
sendNativeTransaction,
sendBzzTransaction,
+20 -19
View File
@@ -1,25 +1,26 @@
import { BZZ, DAI, EthAddress } from '@ethersphere/bee-js'
import { providers, Wallet } from 'ethers'
import { estimateNativeTransferTransactionCost, Rpc } from './rpc'
import { JsonRpcProvider, Wallet } from 'ethers'
import { estimateNativeTransferTransactionCost, RPC } from './rpc'
export class WalletAddress {
private constructor(
public address: string,
public bzz: BZZ,
public dai: DAI,
public provider: providers.JsonRpcProvider,
public provider: JsonRpcProvider,
) {}
static async make(address: string, provider: providers.JsonRpcProvider): Promise<WalletAddress> {
const bzz = await Rpc._eth_getBalanceERC20(address, provider)
const dai = await Rpc._eth_getBalance(address, provider)
static async make(address: string, provider: JsonRpcProvider): Promise<WalletAddress> {
const bzz = await RPC._eth_getBalanceERC20(address, provider)
const dai = await RPC._eth_getBalance(address, provider)
return new WalletAddress(address, bzz, dai, provider)
}
public async refresh(): Promise<WalletAddress> {
this.bzz = await Rpc._eth_getBalanceERC20(this.address, this.provider)
this.dai = 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
}
@@ -33,43 +34,43 @@ export class ResolvedWallet {
public wallet: Wallet,
public bzz: BZZ,
public dai: DAI,
public provider: providers.JsonRpcProvider,
public provider: JsonRpcProvider,
) {
this.address = wallet.address
this.privateKey = wallet.privateKey
}
static async make(privateKeyOrWallet: string | Wallet, provider: providers.JsonRpcProvider): Promise<ResolvedWallet> {
static async make(privateKeyOrWallet: string | Wallet, provider: JsonRpcProvider): Promise<ResolvedWallet> {
const wallet =
typeof privateKeyOrWallet === 'string' ? new Wallet(privateKeyOrWallet, provider) : privateKeyOrWallet
const address = wallet.address
const bzz = await Rpc._eth_getBalanceERC20(address, provider)
const dai = 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 = await Rpc._eth_getBalanceERC20(this.address, this.provider)
this.dai = 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: EthAddress | string, jsonRpcProvider: string): Promise<void> {
public async transfer(destination: EthAddress | string, jsonRpcProviderUrl: string): Promise<void> {
if (this.bzz.gt(BZZ.fromDecimalString('0.05'))) {
await Rpc.sendBzzTransaction(this.privateKey, destination, this.bzz, jsonRpcProvider)
await RPC.sendBzzTransaction(this.privateKey, destination, this.bzz, jsonRpcProviderUrl)
await this.refresh()
}
const { gasPrice, totalCost } = await estimateNativeTransferTransactionCost(this.privateKey, jsonRpcProvider)
const { gasPrice, totalCost } = await estimateNativeTransferTransactionCost(this.privateKey, jsonRpcProviderUrl)
if (this.dai.gt(totalCost)) {
await Rpc.sendNativeTransaction(
await RPC.sendNativeTransaction(
this.privateKey,
destination,
this.dai.minus(totalCost),
jsonRpcProvider,
jsonRpcProviderUrl,
gasPrice,
)
await this.refresh()