From d54b374848214fceb3ecd1df8be074ab0ad5cb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Fri, 10 Jul 2026 13:37:12 +0200 Subject: [PATCH 1/9] chore: strip down eslint to essential --- .eslintrc.js | 75 +++++++-------------------------------------------- package.json | 1 - tsconfig.json | 10 +++++++ 3 files changed, 20 insertions(+), 66 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 86b072c4..31208d0c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,6 +3,9 @@ module.exports = { parserOptions: { sourceType: 'module', ecmaVersion: 2018, + // required for the type-aware rules below (no-floating-promises / no-misused-promises) + project: ['./tsconfig.json', './tsconfig.test.json'], + tsconfigRootDir: __dirname, }, env: { jest: true, @@ -13,78 +16,20 @@ module.exports = { }, plugins: ['jest'], rules: { - 'array-bracket-newline': ['error', 'consistent'], - strict: ['error', 'safe'], - curly: 'error', - 'block-scoped-var': 'error', - complexity: 'warn', - 'default-case': 'error', - 'dot-notation': 'warn', + // --- real bug protection only; formatting is left to Prettier and correctness to tsc --- + curly: ['error', 'multi-line'], eqeqeq: 'error', - 'guard-for-in': 'warn', - 'linebreak-style': ['warn', 'unix'], - 'no-alert': 'error', + 'default-case': 'error', + 'guard-for-in': 'error', 'no-case-declarations': 'error', 'no-console': 'error', 'no-constant-condition': 'error', - 'no-div-regex': 'error', - 'no-empty': 'warn', 'no-empty-pattern': 'error', - 'no-implicit-coercion': 'error', - 'prefer-arrow-callback': 'warn', - 'no-labels': 'error', 'no-loop-func': 'error', - 'no-nested-ternary': 'warn', - 'no-script-url': 'error', - 'no-warning-comments': 'warn', - 'quote-props': ['error', 'as-needed'], 'require-yield': 'error', - 'max-nested-callbacks': ['error', 4], - 'max-depth': ['error', 4], - 'require-await': 'error', - 'space-before-function-paren': [ - 'error', - { - anonymous: 'never', - named: 'never', - asyncArrow: 'always', - }, - ], - 'padding-line-between-statements': [ - 'error', - { blankLine: 'always', prev: '*', next: 'if' }, - { blankLine: 'always', prev: '*', next: 'function' }, - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-useless-constructor': 'off', - 'no-dupe-class-members': 'off', - 'no-unused-expressions': 'off', - curly: ['error', 'multi-line'], - 'object-curly-spacing': ['error', 'always'], - 'comma-dangle': ['error', 'always-multiline'], - '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'none', - requireLast: true, - }, - singleline: { - delimiter: 'comma', - requireLast: false, - }, - }, - ], + // the headline reason to keep ESLint: tsc cannot catch these + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', }, - overrides: [ - { - files: ['*.spec.ts'], - rules: { - // '@typescript-eslint/ban-ts-ignore': 'off', - 'max-nested-callbacks': ['error', 10], // allow describe/it nesting - }, - }, - ], } diff --git a/package.json b/package.json index 0c9dfc29..fff1ce3f 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-jest": "^27.1.5", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-unused-imports": "^2.0.0", "husky": "^4.3.6", "jest": "^29.3.1", "prettier": "^2.7.1", diff --git a/tsconfig.json b/tsconfig.json index e42387d3..4eebbf53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,16 @@ "module": "commonjs", "outDir": "dist", "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, From 0c3b7920586f9e196542f560aeac220e80f9bf5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Fri, 10 Jul 2026 14:08:07 +0200 Subject: [PATCH 2/9] fix: violations according to new ruleset --- .eslintignore | 2 ++ .eslintrc.js | 9 +++++++++ src/command/access/history.ts | 5 +++-- src/command/download.ts | 4 ++++ src/command/feed/feed-command.ts | 11 +++++++++-- src/command/pinning/pinning-command.ts | 2 +- src/command/pss/pss-command.ts | 2 +- src/command/root-command/index.ts | 3 ++- src/command/upload.ts | 2 +- src/index.ts | 3 ++- src/service/access/index.ts | 2 +- src/service/identity/index.ts | 4 ++-- src/service/stamp/index.ts | 2 +- src/utils/bzz-address.ts | 2 +- src/utils/text.ts | 5 ++--- test/command/addresses.spec.ts | 12 ++++++------ test/command/feed.spec.ts | 2 +- test/command/manifest.spec.ts | 2 +- test/command/pss.spec.ts | 4 +++- test/command/upload.spec.ts | 8 ++++---- test/custom-matcher.ts | 2 +- test/utility/index.ts | 4 ++-- test/utility/stamp.ts | 6 ++++-- tsconfig.test.json | 1 + 24 files changed, 64 insertions(+), 35 deletions(-) diff --git a/.eslintignore b/.eslintignore index 06c5e168..8052eefa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,3 @@ dist/** +# upload fixtures — sample project trees used as test payloads, not real source +test/data/** diff --git a/.eslintrc.js b/.eslintrc.js index 31208d0c..60e11e51 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,4 +32,13 @@ module.exports = { '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', }, + overrides: [ + { + // tests legitimately assert on values they have set up + files: ['test/**/*.ts'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + }, + }, + ], } diff --git a/src/command/access/history.ts b/src/command/access/history.ts index 78fe8852..e2f2b368 100644 --- a/src/command/access/history.ts +++ b/src/command/access/history.ts @@ -20,13 +20,14 @@ export class History extends AccessCommand implements LeafCommand { const accessHistory = new AccessHistory(this.commandConfig, this.console) const events = accessHistory.getEvents(this.listName).sort((a, b) => b.createdAt - a.createdAt) - if (events.length === 0) { + const lastEvent = events[0] + + if (!lastEvent) { this.console.error(errorText(`Grantee list with name '${this.listName}' does not exist!`)) exit(1) } - const lastEvent = events[0] this.console.log(createKeyValue('Latest history address', lastEvent.historyAddress)) this.console.log(createKeyValue('Latest grantee list reference', lastEvent.granteeListRef)) diff --git a/src/command/download.ts b/src/command/download.ts index 774ce23b..6c2d115e 100644 --- a/src/command/download.ts +++ b/src/command/download.ts @@ -34,6 +34,10 @@ export class Download extends RootCommand implements LeafCommand { if (this.manifestDownload.access) { const [publisher, historyAddress] = this.manifestDownload.access.split(':') + if (!publisher || !historyAddress) { + this.console.error('Invalid access format. Expected format: publisher:historyAddress') + process.exit(1) + } const responseAct = await this.bee.downloadFile(this.address.hash, this.manifestDownload.destination, { actPublisher: publisher, actHistoryAddress: historyAddress, diff --git a/src/command/feed/feed-command.ts b/src/command/feed/feed-command.ts index bbbe26aa..34274a42 100644 --- a/src/command/feed/feed-command.ts +++ b/src/command/feed/feed-command.ts @@ -58,7 +58,7 @@ export class FeedCommand extends RootCommand { this.console.log(createKeyValue('Feed Manifest URL', manifestUrl)) if (this.qr) { - printQRCodeWithLabel(publicUrl(manifestUrl), 'QR for Manifest URL', this.console) + await printQRCodeWithLabel(publicUrl(manifestUrl), 'QR for Manifest URL', this.console) } this.console.quiet(manifest.toHex()) @@ -92,7 +92,14 @@ export class FeedCommand extends RootCommand { this.identity = await pickIdentity(this.commandConfig, this.console) } - return identities[this.identity] + const identity = identities[this.identity] + + if (!identity) { + this.console.error('The provided identity does not exist.') + exit(1) + } + + return identity } private async writeFeed(stamp: string, wallet: Wallet, topic: Topic, chunkReference: Reference): Promise { diff --git a/src/command/pinning/pinning-command.ts b/src/command/pinning/pinning-command.ts index 2179191e..0dffbd2e 100644 --- a/src/command/pinning/pinning-command.ts +++ b/src/command/pinning/pinning-command.ts @@ -2,7 +2,7 @@ import { exit } from 'process' import { RootCommand } from '../root-command' export class PinningCommand extends RootCommand { - protected async init(): Promise { + protected override async init(): Promise { super.init() if (await this.bee.isGateway()) { diff --git a/src/command/pss/pss-command.ts b/src/command/pss/pss-command.ts index 34b49a52..b02db657 100644 --- a/src/command/pss/pss-command.ts +++ b/src/command/pss/pss-command.ts @@ -14,7 +14,7 @@ export class PssCommand extends RootCommand { }) public topicString!: string - public init(): void { + public override init(): void { super.init() this.topic = Topic.fromString(this.topicString).toHex() diff --git a/src/command/root-command/index.ts b/src/command/root-command/index.ts index 42d9758d..e9325d50 100644 --- a/src/command/root-command/index.ts +++ b/src/command/root-command/index.ts @@ -84,7 +84,8 @@ export class RootCommand { const latestVersionCheck = getLatestVersionCheck(this.commandConfig) if (latestVersionCheck === null) { - checkForUpdates(this.commandConfig) + // fire-and-forget: a version check must not block CLI startup; it swallows its own errors + void checkForUpdates(this.commandConfig) } else if (latestVersionCheck.latestVersion !== PackageJson.version) { this.console.log( warningText( diff --git a/src/command/upload.ts b/src/command/upload.ts index b63d0500..80deeaa1 100644 --- a/src/command/upload.ts +++ b/src/command/upload.ts @@ -213,7 +213,7 @@ export class Upload extends RootCommand implements LeafCommand { } if (this.qr) { - printQRCodeWithLabel(publicUrl(url), 'QR for URL', this.console) + await printQRCodeWithLabel(publicUrl(url), 'QR for URL', this.console) } if (this.usingACT()) { diff --git a/src/index.ts b/src/index.ts index 740be82d..d105cac3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,8 @@ if (setDefaultResultOrder) { setDefaultResultOrder('ipv4first') } -cli({ +// fire-and-forget entry point: cli() reports its own failures via the errorHandler above +void cli({ rootCommandClasses, optionParameters, printer, diff --git a/src/service/access/index.ts b/src/service/access/index.ts index 5979be67..bbfe9496 100644 --- a/src/service/access/index.ts +++ b/src/service/access/index.ts @@ -44,7 +44,7 @@ export class AccessHistory { public getLatestEvent(granteeListName: string): AccessHistoryEvent | null { const events = this.getEvents(granteeListName).sort((a, b) => b.createdAt - a.createdAt) - return events.length > 0 ? events[0] : null + return events[0] ?? null } public getEventsByType(granteeListName: string, eventType: AccessHistoryOperation): AccessHistoryEvent[] { diff --git a/src/service/identity/index.ts b/src/service/identity/index.ts index 0d24aeeb..27693936 100644 --- a/src/service/identity/index.ts +++ b/src/service/identity/index.ts @@ -17,11 +17,11 @@ export function getPrintableIdentityType(type: IdentityType): string { } } -export function isSimpleWallet(wallet: IdentityWallet, identityType: IdentityType): wallet is SimpleWallet { +export function isSimpleWallet(_wallet: IdentityWallet, identityType: IdentityType): _wallet is SimpleWallet { return identityType === IdentityType.simple } -export function isV3Wallet(wallet: IdentityWallet, identityType: IdentityType): wallet is V3Keystore { +export function isV3Wallet(_wallet: IdentityWallet, identityType: IdentityType): _wallet is V3Keystore { return identityType === IdentityType.v3 } diff --git a/src/service/stamp/index.ts b/src/service/stamp/index.ts index 188c64b1..6d707144 100644 --- a/src/service/stamp/index.ts +++ b/src/service/stamp/index.ts @@ -34,7 +34,7 @@ export async function pickStamp(bee: Bee, console: CommandLog): Promise const value = await console.promptList(choices, 'Please select a stamp for this action') const [hex] = value.split(' ') - return hex + return hex ?? value } interface PrintStampSettings { diff --git a/src/utils/bzz-address.ts b/src/utils/bzz-address.ts index b7d31a3b..a210bfb4 100644 --- a/src/utils/bzz-address.ts +++ b/src/utils/bzz-address.ts @@ -15,7 +15,7 @@ export class BzzAddress { throw new CommandLineError('Invalid BZZ path: cannot contain multiple continuous slashes') } const parts = url.split('/') - this.hash = parts[0].toLowerCase() + this.hash = (parts[0] ?? '').toLowerCase() if (this.hash.startsWith('0x')) { this.hash = this.hash.slice(2) diff --git a/src/utils/text.ts b/src/utils/text.ts index ac3ce77e..39f3821f 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -50,12 +50,11 @@ export function printDivided( printFn: (item: T, console: CommandLog) => void, console: CommandLog, ): void { - for (let i = 0; i < items.length; i++) { - const item = items[i] + items.forEach((item, i) => { printFn(item, console) if (i !== items.length - 1) { console.divider() } - } + }) } diff --git a/test/command/addresses.spec.ts b/test/command/addresses.spec.ts index 28e94c29..34aba69e 100644 --- a/test/command/addresses.spec.ts +++ b/test/command/addresses.spec.ts @@ -15,12 +15,12 @@ describeCommand( it('should be splittable in quiet mode', async () => { await invokeTestCli(['addresses', '-q']) - expect(consoleMessages[0].split(' ')[1]).toMatch(/[0-9a-f]{20}/i) - expect(consoleMessages[1].split(' ')[1]).toMatch(/[0-9a-f]{20}/i) - expect(consoleMessages[2].split(' ')[1]).toMatch(/[0-9a-f]{20}/i) - expect(consoleMessages[3].split(' ')[1]).toMatch(/[0-9a-f]{20}/i) - expect(consoleMessages[4].split(' ')[1]).toContain('/ip4/') - expect(consoleMessages[5].split(' ')[1]).toMatch(/[0-9a-f]{20}/i) + expect(consoleMessages[0]!.split(' ')[1]).toMatch(/[0-9a-f]{20}/i) + expect(consoleMessages[1]!.split(' ')[1]).toMatch(/[0-9a-f]{20}/i) + expect(consoleMessages[2]!.split(' ')[1]).toMatch(/[0-9a-f]{20}/i) + expect(consoleMessages[3]!.split(' ')[1]).toMatch(/[0-9a-f]{20}/i) + expect(consoleMessages[4]!.split(' ')[1]).toContain('/ip4/') + expect(consoleMessages[5]!.split(' ')[1]).toMatch(/[0-9a-f]{20}/i) }) }, { configFileName: 'addresses' }, diff --git a/test/command/feed.spec.ts b/test/command/feed.spec.ts index f01a0bcb..ac6c0dda 100644 --- a/test/command/feed.spec.ts +++ b/test/command/feed.spec.ts @@ -48,7 +48,7 @@ describeCommand( it('should print feed using address only', async () => { // create identity await invokeTestCli(['identity', 'create', 'test2', '--password', 'test']) - const address = getLastMessage().split(' ')[1] + const address = getLastMessage().split(' ')[1] ?? '' // upload await invokeTestCli([ 'feed', diff --git a/test/command/manifest.spec.ts b/test/command/manifest.spec.ts index 99242943..c7676827 100644 --- a/test/command/manifest.spec.ts +++ b/test/command/manifest.spec.ts @@ -15,7 +15,7 @@ expect.extend({ }) async function runAndGetManifest(argv: string[]): Promise { - if (['create', 'add', 'sync', 'merge', 'remove'].includes(argv[1])) { + if (['create', 'add', 'sync', 'merge', 'remove'].includes(argv[1] ?? '')) { argv = [...argv, ...getStampOption()] } const commandBuilder = await invokeTestCli(argv) diff --git a/test/command/pss.spec.ts b/test/command/pss.spec.ts index 6884b4c3..8d8c3d49 100644 --- a/test/command/pss.spec.ts +++ b/test/command/pss.spec.ts @@ -45,7 +45,8 @@ describeCommand('Test PSS command', ({ getNthLastMessage, getLastMessage }) => { unlinkSync('test/testconfig/out.txt') } writeFileSync('test/testconfig/in.txt', 'Message in a file') - invokeTestCli([ + // start the receiver listening, then send to trigger it, then await the receiver + const receivePromise = invokeTestCli([ 'pss', 'receive', '--topic-string', @@ -68,6 +69,7 @@ describeCommand('Test PSS command', ({ getNthLastMessage, getLastMessage }) => { 'test/testconfig/in.txt', ...getStampOption(), ]) + await receivePromise await System.sleepMillis(4000) expect(existsSync('test/testconfig/out.txt')).toBeTruthy() const messageFromFile = readFileSync('test/testconfig/out.txt', 'ascii') diff --git a/test/command/upload.spec.ts b/test/command/upload.spec.ts index 51802b7f..d15e768e 100644 --- a/test/command/upload.spec.ts +++ b/test/command/upload.spec.ts @@ -41,8 +41,8 @@ describeCommand( const uploadFolderPath = `${__dirname}/../testpage` const commandBuilder = await invokeTestCli([commandKey, uploadFolderPath, ...getStampOption()]) - expect(commandBuilder.initedCommands[0].command.name).toBe('upload') - const command = commandBuilder.initedCommands[0].command as Upload + expect(commandBuilder.initedCommands[0]!.command.name).toBe('upload') + const command = commandBuilder.initedCommands[0]!.command as Upload expect(command.result.getOrThrow().toHex().length).toBe(64) }) @@ -51,8 +51,8 @@ describeCommand( const uploadFolderPath = `${__dirname}/../testpage/images/swarm.png` const commandBuilder = await invokeTestCli([commandKey, uploadFolderPath, ...getStampOption()]) - expect(commandBuilder.initedCommands[0].command.name).toBe('upload') - const command = commandBuilder.initedCommands[0].command as Upload + expect(commandBuilder.initedCommands[0]!.command.name).toBe('upload') + const command = commandBuilder.initedCommands[0]!.command as Upload expect(command.result.getOrThrow().toHex().length).toBe(64) }) diff --git a/test/custom-matcher.ts b/test/custom-matcher.ts index f2b76d57..0dd5cafb 100644 --- a/test/custom-matcher.ts +++ b/test/custom-matcher.ts @@ -23,7 +23,7 @@ export function toMatchLinesInOrder(received: string[], pattern: string[][]) { export function toMatchLinesInAnyOrder(received: string[], pattern: string[][]) { for (const p of pattern) { - if (received.some(r => r.includes(p[0]))) { + if (received.some(r => r.includes(p[0] ?? ''))) { continue } diff --git a/test/utility/index.ts b/test/utility/index.ts index c4736629..d7085802 100644 --- a/test/utility/index.ts +++ b/test/utility/index.ts @@ -31,8 +31,8 @@ export function describeCommand( describe(description, () => { const consoleMessages: string[] = [] const configFileName = options?.configFileName - const getNthLastMessage = (n: number) => consoleMessages[consoleMessages.length - n] - const getLastMessage = () => consoleMessages[consoleMessages.length - 1] + const getNthLastMessage = (n: number) => consoleMessages[consoleMessages.length - n] ?? '' + const getLastMessage = () => consoleMessages[consoleMessages.length - 1] ?? '' const hasMessageContaining = (substring: string) => Boolean(consoleMessages.find(consoleMessage => consoleMessage.includes(substring))) const configFolderPath = join(__dirname, '..', 'testconfig') diff --git a/test/utility/stamp.ts b/test/utility/stamp.ts index 6ff8740c..4be58693 100644 --- a/test/utility/stamp.ts +++ b/test/utility/stamp.ts @@ -6,8 +6,10 @@ export const getOrBuyStamp = async (): Promise => { const bee = new Bee('http://localhost:1633') const availableStamps = await bee.getAllPostageBatch() - if (availableStamps.length > 0) { - const usedStamp = availableStamps[0].batchID + const [existingStamp] = availableStamps + + if (existingStamp) { + const usedStamp = existingStamp.batchID console.log('Using existing stamp: ', usedStamp.toHex()) return usedStamp diff --git a/tsconfig.test.json b/tsconfig.test.json index 24887d62..385c7282 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -5,6 +5,7 @@ "test", "jest.config.ts", ], + "exclude": ["node_modules", "dist", "test/data"], "compilerOptions": { "noEmit": true, "skipLibCheck": true From f7657a377c638a80666324241b7d194c269b1eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Fri, 10 Jul 2026 14:29:26 +0200 Subject: [PATCH 3/9] fix: more violations --- src/command/stake/recover.ts | 4 ++-- src/command/utility/create-batch.ts | 4 ++-- src/utils/rpc.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/command/stake/recover.ts b/src/command/stake/recover.ts index 68950140..2ad7087e 100644 --- a/src/command/stake/recover.ts +++ b/src/command/stake/recover.ts @@ -56,7 +56,7 @@ export class Recover extends RootCommand implements LeafCommand { const { signer } = await makeReadySigner(wallet.getPrivateKeyString(), this.jsonRpcUrl) const contract = new Contract(address, abi, signer) - const isPaused = await contract.paused() + const isPaused = await contract.paused!() if (!isPaused) { this.console.error('The contract is not paused. No need to recover xBZZ.') @@ -65,6 +65,6 @@ export class Recover extends RootCommand implements LeafCommand { } this.console.log('Recovering xBZZ from paused staking contract...') - await contract.migrateStake() + await contract.migrateStake!() } } diff --git a/src/command/utility/create-batch.ts b/src/command/utility/create-batch.ts index be5a16cf..3b58c849 100644 --- a/src/command/utility/create-batch.ts +++ b/src/command/utility/create-batch.ts @@ -65,7 +65,7 @@ export class CreateBatch extends RootCommand implements LeafCommand { 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(), { + const approve = await tokenProxyContract.approve!(Contracts.postageStamp, cost.toPLURBigInt().toString(), { gasLimit: 130_000, type: 2, maxFeePerGas: Numbers.make('2gwei'), @@ -77,7 +77,7 @@ 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 createBatchArgs = [signer.address, this.amount, this.depth, 16, `0x${Strings.randomHex(64)}`, false] - const createBatch = await postageStampContract.createBatch(...createBatchArgs, { + const createBatch = await postageStampContract.createBatch!(...createBatchArgs, { gasLimit: 1_000_000, type: 2, maxFeePerGas: Numbers.make('3gwei'), diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index b30b604a..de8a4701 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -19,7 +19,7 @@ export async function eth_getBalanceERC20( address = `0x${address}` } const contract = new Contract(tokenAddress, ABI.bzz, provider) - const balance = await contract.balanceOf(address) + const balance = await contract.balanceOf!(address) return balance.toString() } @@ -99,7 +99,7 @@ export async function sendBzzTransaction( } const bzz = new Contract(Contracts.bzz, ABI.bzz, signer) - const transaction = await bzz.transfer(to, value, { gasPrice }) + const transaction = await bzz.transfer!(to, value, { gasPrice }) const receipt = await transaction.wait(1) if (receipt === null) { From 1b9e459e7972bbc2d1d2943d0afb8f68635569e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Fri, 10 Jul 2026 14:47:21 +0200 Subject: [PATCH 4/9] fix: remove unnecessary sleep --- test/command/pss.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/command/pss.spec.ts b/test/command/pss.spec.ts index 8d8c3d49..9a2307cf 100644 --- a/test/command/pss.spec.ts +++ b/test/command/pss.spec.ts @@ -70,7 +70,6 @@ describeCommand('Test PSS command', ({ getNthLastMessage, getLastMessage }) => { ...getStampOption(), ]) await receivePromise - await System.sleepMillis(4000) expect(existsSync('test/testconfig/out.txt')).toBeTruthy() const messageFromFile = readFileSync('test/testconfig/out.txt', 'ascii') expect(messageFromFile).toBe('Message in a file') From cd69ed286fc357e51326e5c3e7f5fd0356309905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Fri, 10 Jul 2026 14:56:15 +0200 Subject: [PATCH 5/9] fix: custom matcher --- test/custom-matcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/custom-matcher.ts b/test/custom-matcher.ts index 0dd5cafb..ac26d544 100644 --- a/test/custom-matcher.ts +++ b/test/custom-matcher.ts @@ -23,7 +23,7 @@ export function toMatchLinesInOrder(received: string[], pattern: string[][]) { export function toMatchLinesInAnyOrder(received: string[], pattern: string[][]) { for (const p of pattern) { - if (received.some(r => r.includes(p[0] ?? ''))) { + if (received.some(r => p.every(substring => r.includes(substring)))) { continue } From 8a66ecb87ccdc89ee26d7b39ebf69d5beacf91e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Mon, 10 Aug 2026 12:28:16 +0200 Subject: [PATCH 6/9] fix: typo --- src/command/manifest/sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command/manifest/sync.ts b/src/command/manifest/sync.ts index f66dc518..8680c846 100644 --- a/src/command/manifest/sync.ts +++ b/src/command/manifest/sync.ts @@ -73,7 +73,7 @@ export class Sync extends RootCommand implements LeafCommand { if (this.remove) { for (const n of nodes) { - if (!files.includes(node.fullPathString)) { + if (!files.includes(n.fullPathString)) { node.removeFork(n.fullPathString) this.console.log(chalk.gray(n.fullPathString) + ' ' + chalk.red('REMOVED')) } From 060d0da13d698dc5455b9c6c09ef8525a22a6dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Mon, 10 Aug 2026 12:39:53 +0200 Subject: [PATCH 7/9] fix: lint --- test/command/reupload.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/command/reupload.spec.ts b/test/command/reupload.spec.ts index 61dbf213..6a9d370c 100644 --- a/test/command/reupload.spec.ts +++ b/test/command/reupload.spec.ts @@ -1,10 +1,10 @@ import { readFileSync } from 'fs' import type { Reupload } from '../../src/command/reupload' +import { Buy } from '../../src/command/stamp/buy' import type { Upload } from '../../src/command/upload' import { HistoryItem } from '../../src/service/history/types/history-item' import { describeCommand, invokeTestCli } from '../utility' import { getStampOption } from '../utility/stamp' -import { Buy } from '../../src/command/stamp/buy' describeCommand( 'Test Reupload command', @@ -49,14 +49,14 @@ describeCommand( // 4. Verify the history entry const reuploadCommand = reuploadBuilder.runnable as Reupload - const historyPath = (reuploadCommand as any).commandConfig.getHistoryFilePath() + const historyPath = reuploadCommand.commandConfig.getHistoryFilePath() const items = JSON.parse(readFileSync(historyPath, 'utf-8')) as HistoryItem[] const lastItem = items[items.length - 1] - expect(lastItem.reference).toBe(reference) - expect(lastItem.uploadType).toBe('reupload') - expect(lastItem.path).toBeNull() - expect(lastItem.stamp).toBe(newStampId) + expect(lastItem!.reference).toBe(reference) + expect(lastItem!.uploadType).toBe('reupload') + expect(lastItem!.path).toBeNull() + expect(lastItem!.stamp).toBe(newStampId) }) }, { configFileName: 'reupload' }, From d4da78d207b23481a2ea0669fc6ec3596c1abbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Mon, 10 Aug 2026 13:55:02 +0200 Subject: [PATCH 8/9] fix: manifest sync logic --- package-lock.json | 73 ++++++++++++++++++++---------------- package.json | 1 + src/command/manifest/sync.ts | 66 +++++++++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2aa3e2f..1b1f804c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,11 +6,12 @@ "packages": { "": { "name": "@ethersphere/swarm-cli", - "version": "3.3.0", + "version": "3.4.0", "license": "BSD-3-Clause", "dependencies": { "@ethereumjs/wallet": "^2.0.4", "@ethersphere/bee-js": "^12.0.0", + "@upcoming/swarm-core": "^0.0.8", "cafe-utility": "^33.3.1", "chalk": "^2.4.2", "cli-progress": "^3.11.2", @@ -41,7 +42,6 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-jest": "^27.1.5", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-unused-imports": "^2.0.0", "husky": "^4.3.6", "jest": "^29.3.1", "prettier": "^2.7.1", @@ -2535,6 +2535,45 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@upcoming/swarm-core": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@upcoming/swarm-core/-/swarm-core-0.0.8.tgz", + "integrity": "sha512-gLiHUAUib7dDSM1KqHzKSTWIBx/8zXDOU1K6kXXft3R9tAGCahK/uLYnVbPOu28UKkY49Wfon9fQbnyf7dusxQ==", + "dependencies": { + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@upcoming/swarm-core/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@upcoming/swarm-core/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/acorn": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", @@ -3722,36 +3761,6 @@ } } }, - "node_modules/eslint-plugin-unused-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", - "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", - "dev": true, - "dependencies": { - "eslint-rule-composer": "^0.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", - "eslint": "^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - } - } - }, - "node_modules/eslint-rule-composer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index fff1ce3f..eed614b8 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "dependencies": { "@ethereumjs/wallet": "^2.0.4", "@ethersphere/bee-js": "^12.0.0", + "@upcoming/swarm-core": "^0.0.8", "cafe-utility": "^33.3.1", "chalk": "^2.4.2", "cli-progress": "^3.11.2", diff --git a/src/command/manifest/sync.ts b/src/command/manifest/sync.ts index 8680c846..f1387550 100644 --- a/src/command/manifest/sync.ts +++ b/src/command/manifest/sync.ts @@ -1,4 +1,5 @@ -import { MantarayNode, MerkleTree } from '@ethersphere/bee-js' +import { MantarayNode, RedundancyLevel } from '@ethersphere/bee-js' +import { ChunkSplitter, makeErasureBatch, makeIntermediateChunkHandler } from '@upcoming/swarm-core' import { Binary, Optional } from 'cafe-utility' import chalk from 'chalk' import { readFileSync } from 'fs' @@ -7,9 +8,17 @@ import { join } from 'path' import { pickStamp } from '../../service/stamp' import { readdirDeepAsync } from '../../utils' import { BzzAddress } from '../../utils/bzz-address' +import { CommandLineError } from '../../utils/error' import { stampProperties } from '../../utils/option' import { RootCommand } from '../root-command' +// Bee applies this level itself whenever a client doesn't ask for a specific one +// (verified against a real node - bee-js's own docs claim OFF is the server default, +// which does not match observed behavior). Assumed here so the local pre-check below +// can replicate the same erasure coding Bee applies, rather than compare against a +// bare, non-redundant hash that will never match a redundant upload. +const DEFAULT_REDUNDANCY_LEVEL = RedundancyLevel.MEDIUM + export class Sync extends RootCommand implements LeafCommand { public readonly name = 'sync' public readonly description = 'Sync a local folder to an existing manifest' @@ -30,6 +39,41 @@ export class Sync extends RootCommand implements LeafCommand { }) public remove!: boolean + @Option({ + key: 'redundancy', + description: 'Redundancy of the upload (OFF, MEDIUM, STRONG, INSANE, PARANOID)', + }) + public redundancy!: string + + private determineRedundancyLevel(): RedundancyLevel | undefined { + if (!this.redundancy) { + return undefined + } + switch (this.redundancy.toUpperCase()) { + case 'OFF': + return RedundancyLevel.OFF + case 'MEDIUM': + return RedundancyLevel.MEDIUM + case 'STRONG': + return RedundancyLevel.STRONG + case 'INSANE': + return RedundancyLevel.INSANE + case 'PARANOID': + return RedundancyLevel.PARANOID + default: + throw new CommandLineError(`Invalid redundancy level: ${this.redundancy}`) + } + } + + private async expectedReference(data: Uint8Array, level: RedundancyLevel): Promise { + const onBatch = makeErasureBatch(level, false, async () => {}) + const splitter = new ChunkSplitter(onBatch, undefined, false, makeIntermediateChunkHandler(level)) + await splitter.append(data) + const root = await splitter.finalize() + + return root.hash().toUint8Array() + } + public async run(): Promise { super.init() @@ -37,6 +81,13 @@ export class Sync extends RootCommand implements LeafCommand { this.stamp = await pickStamp(this.bee, this.console) } + const requestedRedundancyLevel = this.determineRedundancyLevel() + const effectiveRedundancyLevel = requestedRedundancyLevel ?? DEFAULT_REDUNDANCY_LEVEL + const uploadOptions = + requestedRedundancyLevel === undefined + ? undefined + : { headers: { 'swarm-redundancy-level': String(requestedRedundancyLevel) } } + const address = new BzzAddress(this.bzzUrl) const node = await MantarayNode.unmarshal(this.bee, address.hash) @@ -55,17 +106,22 @@ export class Sync extends RootCommand implements LeafCommand { if (existing) { const localData = readFileSync(join(this.folder, file)) - const rootChunk = await MerkleTree.root(localData) + const expected = await this.expectedReference(new Uint8Array(localData), effectiveRedundancyLevel) - if (Binary.equals(rootChunk.hash(), existing.targetAddress)) { + if (Binary.equals(expected, existing.targetAddress)) { this.console.log(chalk.gray(file) + ' ' + chalk.blue('UNCHANGED')) } else { - const { reference } = await this.bee.uploadData(this.stamp, localData) + const { reference } = await this.bee.uploadData(this.stamp, localData, undefined, uploadOptions) node.addFork(file, reference) this.console.log(chalk.gray(file) + ' ' + chalk.yellow('CHANGED')) } } else { - const { reference } = await this.bee.uploadData(this.stamp, readFileSync(join(this.folder, file))) + const { reference } = await this.bee.uploadData( + this.stamp, + readFileSync(join(this.folder, file)), + undefined, + uploadOptions, + ) node.addFork(file, reference) this.console.log(chalk.gray(file) + ' ' + chalk.green('NEW')) } From 183cc05d50a4013455aaa35fee13522a85281d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20B=C3=A9k=C3=A9si?= Date: Mon, 10 Aug 2026 14:07:34 +0200 Subject: [PATCH 9/9] fix: lint --- src/command/manifest/sync.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/command/manifest/sync.ts b/src/command/manifest/sync.ts index f1387550..0c36d90d 100644 --- a/src/command/manifest/sync.ts +++ b/src/command/manifest/sync.ts @@ -66,7 +66,9 @@ export class Sync extends RootCommand implements LeafCommand { } private async expectedReference(data: Uint8Array, level: RedundancyLevel): Promise { - const onBatch = makeErasureBatch(level, false, async () => {}) + const onBatch = makeErasureBatch(level, false, async () => { + // no-op: only the resulting hash is needed here, nothing to persist + }) const splitter = new ChunkSplitter(onBatch, undefined, false, makeIntermediateChunkHandler(level)) await splitter.append(data) const root = await splitter.finalize() @@ -81,12 +83,12 @@ export class Sync extends RootCommand implements LeafCommand { this.stamp = await pickStamp(this.bee, this.console) } - const requestedRedundancyLevel = this.determineRedundancyLevel() - const effectiveRedundancyLevel = requestedRedundancyLevel ?? DEFAULT_REDUNDANCY_LEVEL - const uploadOptions = - requestedRedundancyLevel === undefined - ? undefined - : { headers: { 'swarm-redundancy-level': String(requestedRedundancyLevel) } } + // Always sent explicitly, even when --redundancy is omitted: leaving this to the + // node's own implicit default would make the comparison below only as reliable as + // a guess about that node's behavior, which we've seen differ between Bee versions + // (2.6.0 defaults to OFF, 2.8.1+ to MEDIUM). Fixing it here removes the ambiguity. + const effectiveRedundancyLevel = this.determineRedundancyLevel() ?? DEFAULT_REDUNDANCY_LEVEL + const uploadOptions = { headers: { 'swarm-redundancy-level': String(effectiveRedundancyLevel) } } const address = new BzzAddress(this.bzzUrl)