diff --git a/docs/reviews/binary-payload-broadcast-summary.md b/docs/reviews/binary-payload-broadcast-summary.md new file mode 100644 index 0000000..aabbbc3 --- /dev/null +++ b/docs/reviews/binary-payload-broadcast-summary.md @@ -0,0 +1,84 @@ +# Architect Review — binary-payload-broadcast + +**Reviewed commits** +- `62e0479` Specify binary hash160 broadcast payload for follow/mute (specifier) +- `5e1f473` Broadcast follow/mute/unfollow/unmute hash160 payloads as Uint8Array (coder) +- `53f6b90` / `8d40f2b` Deduplicate Memo follow/mute onto shared MemoStateAction base (refactorer) + +**Task**: Follow/unfollow and mute/unmute must broadcast the target's raw +20-byte hash160 (not its display-form cash address text) as the OP_RETURN +payload, and must not depend on the Node-only `Buffer` global. The refactorer +also consolidated the previously near-identical `MemoFollow`/`MemoMute` action +classes onto a shared `MemoStateAction` base. + +## Architectural findings + +**UI/Core separation — good.** The follow/mute services remain pure logic over +injected `wallet`/`profiles` adapters. No UI, framework, or IO leaked into the +core; everything network/UI-specific stays behind the small wallet/profile +adapter boundaries. The modules remain fully testable without launching a UI or +network. + +**Dependency rule — good.** `MemoFollow`/`MemoMute` → `MemoStateAction` → +`MemoAction` → `hex`/`utf8`. High-level action semantics depend on low-level +byte/hex helpers through a stable base; the direction is inward. The base +subclasses (`MemoFollow`, `MemoMute`) are thin facades exposing only +`follow`/`unfollow` and `mute`/`unmute`. + +**Information hiding / encapsulation — good.** `MemoStateAction` encapsulates +the shared validate → toHash160 → hexToBytes → sendOpReturn → reflect +transition. Subclasses expose only config (`followConfig`/`muteConfig`) and +their two public methods; static exports and the public API are preserved. The +20-byte length lives on the base as `PK_HASH_LENGTH`, surfaced on both +subclasses. + +**Local code quality.** The refactor is a clean DRY extraction (~150 duplicate +lines removed). Two minor, pre-existing notes (not requiring change): +- Each config carries a `prefix` key that is unused (state actions pass the + prefix explicitly to `_setState`); harmless dead config carried over from the + prior `MemoAction` contract. +- `MemoStateAction.validate` throws a typed error whereas the base + `MemoAction.validate` returns `{ok:false}`; contract difference is intended + and documented in the module header. + +No architectural changes were required. The merged work confirms and preserves +the earlier binary `Uint8Array` broadcast behavior (`hexToBytes`) and the +Buffer-free production source. + +## Verification + +All per-component verification for `psf-memo-client` (the only touched +component) passed. + +- **Unit tests**: 280/280 pass +- **Property tests**: 40/40 pass (run separately, as required) +- **Lint**: clean (`standard`) +- **Acceptance**: all 23 generated feature suites pass, including the new + `binary-payload-broadcast.feature` +- **Language mutation** (`mutate4javascript --mutate-all --max-workers 8`): + - `memo-follow.js`: 2 killed, 0 survived, 0 uncovered + - `memo-mute.js`: 2 killed, 0 survived, 0 uncovered + - `memo-state-action.js` (new base): 5 killed, 0 survived, 0 uncovered + - `hex.js`/`memo-action.js` unchanged and unaffected. +- **DRY** (`dry4javascript`) on `memo-state-action.js`/`memo-follow.js`/ + `memo-mute.js`: no duplicate candidates. + +## Soft Gherkin acceptance mutation (survivors) + +`gherkin-mutator --level soft` over `binary-payload-broadcast.feature`: +4 mutations run, 4 survived, 0 killed, 0 errors. All four are single-character +**case** mutations of the shared example cash address (`q`→`Q`, `x`→`X`, +`c`→`C`, `k`→`K`). Each mutated example is applied consistently on both the +broadcast setup side and the assertion side of its scenario, so the scenario +still passes under the mutation (both sides derive from the same mutated +value). These are **genuine intrinsic equivalents**, not implementation gaps, +and match the documented read-only-payload survivor pattern. Documented here; +no chase performed. + +## Handoffs + +No functional handoffs sent. The work was a review of an already-functional +refactorer merge; my branch adds only the durable review summary plus the +tool-refreshed manifests (mutation `tested_at` timestamps and the new +acceptance-mutation manifest) from the verification runs. Function and module +hashes in the mutation manifests are unchanged, confirming no code drift. diff --git a/psf-memo-client/acceptance/lib/handlers.js b/psf-memo-client/acceptance/lib/handlers.js index 6637243..c8fdbf2 100644 --- a/psf-memo-client/acceptance/lib/handlers.js +++ b/psf-memo-client/acceptance/lib/handlers.js @@ -98,8 +98,12 @@ function makeWallet (address) { return this.utxos }, sendOpReturn: async function (msg, prefix, bchOutput = []) { - // Record the broadcast attempt, then fail if configured to do so. - this.broadcasts.push({ msg, prefix, bchOutput }) + // Normalize binary payloads to Buffer so assertions can safely use + // toString('hex'), while preserving string payloads unchanged. + const storedMsg = (msg instanceof Uint8Array || ArrayBuffer.isView(msg)) + ? Buffer.from(msg) + : msg + this.broadcasts.push({ msg: storedMsg, prefix, bchOutput }) if (this.failWith) throw new Error(this.failWith) return 'aa'.repeat(32) } @@ -1751,6 +1755,38 @@ const handlers = [ } } }, + { + name: 'broadcasts OP_RETURN with Memo binary hash160 payload for address', + pattern: /^the app broadcasts an OP_RETURN transaction with the Memo (follow|unfollow|mute|unmute) prefix and the binary hash160 payload for the address (.+)$/, + run (m, example, world) { + const action = m[1] + const addr = resolveParam(m[2], example) + const hash160 = world.wallet.bchjs.Address.toHash160(addr) + const prefix = { + follow: MEMO_FOLLOW_PREFIX, + unfollow: MEMO_UNFOLLOW_PREFIX, + mute: MEMO_MUTE_PREFIX, + unmute: MEMO_UNMUTE_PREFIX + }[action] + if (!prefix) { + throw new Error(`Unknown follow/mute action: ${action}`) + } + const broadcasts = world.wallet.broadcasts + if (!broadcasts.length) { + throw new Error('No OP_RETURN transaction was broadcast.') + } + const last = broadcasts[broadcasts.length - 1] + if (last.prefix !== prefix) { + throw new Error(`Expected Memo ${action} prefix ${prefix}, got "${last.prefix}".`) + } + if (last.msg.length !== 20) { + throw new Error(`Broadcast ${action} payload is not 20 bytes.`) + } + if (last.msg.toString('hex') !== hash160) { + throw new Error(`Broadcast ${action} hash160 did not match ${addr}.`) + } + } + }, { name: 'API serves topic with post count', pattern: /^the psf-memo-db API serves a topic named "([^"]+)" with (\d+) posts?$/, diff --git a/psf-memo-client/specs/binary-payload-broadcast.feature b/psf-memo-client/specs/binary-payload-broadcast.feature index c21cea9..49209b3 100644 --- a/psf-memo-client/specs/binary-payload-broadcast.feature +++ b/psf-memo-client/specs/binary-payload-broadcast.feature @@ -1,3 +1,7 @@ +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-09-04T18:29:23.460506768Z","feature_name":"Binary Payload Broadcast","feature_path":"../../psf-memo-client/specs/binary-payload-broadcast.feature","background_hash":"e1d5f81f1ed083ac6934c429ca3cb4a0f8d4dac44c2eaa45c0960920bde2c017","implementation_hash":"unknown","scenarios":[]} +# acceptance-mutation-manifest-end + # Scenarios: Binary Payload Broadcast - 1, Binary Payload Broadcast - 2, Binary Payload Broadcast - 3, Binary Payload Broadcast - 4 # # Follow/unfollow and mute/unmute broadcast an OP_RETURN whose payload is the diff --git a/psf-memo-client/src/services/memo-follow.js b/psf-memo-client/src/services/memo-follow.js index 381f14f..5f7f069 100644 --- a/psf-memo-client/src/services/memo-follow.js +++ b/psf-memo-client/src/services/memo-follow.js @@ -16,105 +16,44 @@ PK_HASH_LENGTH : size of the followee hash160 in bytes (20) */ -const MemoAction = require('./memo-action') +const MemoStateAction = require('./memo-state-action') const MEMO_FOLLOW_PREFIX = '6d06' const MEMO_UNFOLLOW_PREFIX = '6d07' -const PK_HASH_LENGTH = 20 -class MemoFollow extends MemoAction { - static config = { +// Compose the static config for this Memo state action. +function followConfig () { + return { prefix: MEMO_FOLLOW_PREFIX, walletRequiredMsg: 'Memo follow requires a wallet.', lengthMessage: 'Follow address is invalid.', emptyMessage: 'Follow address is required.', lengthCode: 'follow_validation', - validationCode: 'follow_validation' + validationCode: 'follow_validation', + reflectMethod: 'setFollowState' } +} - constructor (deps = {}) { - super(deps) - this.profiles = deps.profiles - } - - // Validate a candidate cash address. Returns { ok: true } or throws a typed - // validation error. - validate (addr) { - if (typeof addr !== 'string' || addr.trim().length === 0) { - const err = new Error(this.emptyMessage) - err.code = this.validationCode - throw err - } - - try { - this._toHash160(addr) - return { ok: true } - } catch (err) { - const validationErr = new Error(`Invalid cash address: ${addr}`) - validationErr.code = this.validationCode - throw validationErr - } - } +class MemoFollow extends MemoStateAction { + static config = followConfig() // Broadcast a Memo follow for the given followee address. async follow (followeeAddr) { - return this._broadcastAction(followeeAddr, MEMO_FOLLOW_PREFIX, true) + return this._setState(followeeAddr, MEMO_FOLLOW_PREFIX, true) } // Broadcast a Memo unfollow for the given followee address. async unfollow (followeeAddr) { - return this._broadcastAction(followeeAddr, MEMO_UNFOLLOW_PREFIX, false) - } - - // Internal: validate, broadcast, and reflect a follow/unfollow action. - async _broadcastAction (followeeAddr, prefix, isFollow) { - if (!this.wallet) { - throw new Error(this.walletRequiredMsg) - } - this._ensureBchjs() - - this.validate(followeeAddr) - - await this.wallet.getUtxos() - - const hash160 = this._toHash160(followeeAddr) - const raw = Buffer.from(hash160, 'hex') - - const txid = await this.wallet.sendOpReturn(raw, prefix) - - this.reflect(txid, followeeAddr, isFollow) - - return txid - } - - // Convert a cash address to its 20-byte hash160 hex string using the wallet's - // embedded bch-js. - _toHash160 (addr) { - return this.wallet.bchjs.Address.toHash160(addr) - } - - _ensureBchjs () { - if (!this.wallet.bchjs || typeof this.wallet.bchjs.Address.toHash160 !== 'function') { - throw new Error('Wallet does not expose bch-js Address.toHash160.') - } - } - - // Record the new follow state on the injected profile store when it exposes - // the follow methods. - reflect (txid, followeeAddr, isFollow) { - if (this.profiles && typeof this.profiles.setFollowState === 'function') { - const myAddr = this.wallet?.walletInfo?.cashAddress - this.profiles.setFollowState(myAddr, followeeAddr, isFollow) - } + return this._setState(followeeAddr, MEMO_UNFOLLOW_PREFIX, false) } } MemoFollow.MEMO_FOLLOW_PREFIX = MEMO_FOLLOW_PREFIX MemoFollow.MEMO_UNFOLLOW_PREFIX = MEMO_UNFOLLOW_PREFIX -MemoFollow.PK_HASH_LENGTH = PK_HASH_LENGTH +MemoFollow.PK_HASH_LENGTH = MemoStateAction.PK_HASH_LENGTH module.exports = MemoFollow // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-27T17:51:36.253Z","module_hash":"fc0d7db2a5f7872ae0d9228a19c7314b4f46f7574c53b2a168a62b986c43a31e","functions":[{"id":"func/MemoFollow.constructor","name":"MemoFollow.constructor","line":35,"end_line":38,"hash":"9407b43605444074011b1da595c9d53356352c72b0d847e00365d79ad705663a"},{"id":"func/MemoFollow.validate","name":"MemoFollow.validate","line":42,"end_line":57,"hash":"4154af442c36324ba147dff352c55610397fb430c9b62fbbe44728ab0e379179"},{"id":"func/MemoFollow.follow","name":"MemoFollow.follow","line":60,"end_line":62,"hash":"e24f69ef420d1ef625c2d03eae6014c824fdb35438a87e379fdc41747bbd321d"},{"id":"func/MemoFollow.unfollow","name":"MemoFollow.unfollow","line":65,"end_line":67,"hash":"48e93e85645bbcd767467afb807625b4adf6d6076f02fc841c91f58147bb44c7"},{"id":"func/MemoFollow._broadcastAction","name":"MemoFollow._broadcastAction","line":70,"end_line":88,"hash":"3720e2b6d78265383728bd7c5fcafeb018fad9c17a8bede12a6d4ffc3415cd3a"},{"id":"func/MemoFollow._toHash160","name":"MemoFollow._toHash160","line":92,"end_line":94,"hash":"3ed2ba4853b718a8316345c9f988ed3bed3ef647de3fd9761ee3c9e0bee86070"},{"id":"func/MemoFollow._ensureBchjs","name":"MemoFollow._ensureBchjs","line":96,"end_line":100,"hash":"92828d9038c9a4d367e80edd7f129291e434d57aae80b332081a66af9896a5d6"},{"id":"func/MemoFollow.reflect","name":"MemoFollow.reflect","line":104,"end_line":109,"hash":"af3f0ead1a9f51724e3c252122b3bf13c5323a3b9da244dee87a34815a90c94c"}]} +// {"version":1,"tested_at":"2026-09-04T18:27:18.336Z","module_hash":"cc36dc8387a54f50e91393bcb10382515e66a0cfc74fb4f51412fac4df580136","functions":[{"id":"func/followConfig","name":"followConfig","line":25,"end_line":35,"hash":"6843e108f72a95c18267d1ecea29fdfbdb0814ce4b5e3672f22df612e6ecaccd"},{"id":"func/MemoFollow.follow","name":"MemoFollow.follow","line":41,"end_line":43,"hash":"82635824a8e221e587b74b22fea454d2f661e599d8c9b9d0d7ec7c14e214cbd7"},{"id":"func/MemoFollow.unfollow","name":"MemoFollow.unfollow","line":46,"end_line":48,"hash":"fa99fa77d68a875092f5aad8323dca42de06dff49af469c6d7db94f7033399b5"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-client/src/services/memo-mute.js b/psf-memo-client/src/services/memo-mute.js index 892a796..d8f7f20 100644 --- a/psf-memo-client/src/services/memo-mute.js +++ b/psf-memo-client/src/services/memo-mute.js @@ -16,105 +16,44 @@ PK_HASH_LENGTH : size of the mutee hash160 in bytes (20) */ -const MemoAction = require('./memo-action') +const MemoStateAction = require('./memo-state-action') const MEMO_MUTE_PREFIX = '6d16' const MEMO_UNMUTE_PREFIX = '6d17' -const PK_HASH_LENGTH = 20 -class MemoMute extends MemoAction { - static config = { +// Compose the static config for this Memo state action. +function muteConfig () { + return { prefix: MEMO_MUTE_PREFIX, walletRequiredMsg: 'Memo mute requires a wallet.', lengthMessage: 'Mute address is invalid.', emptyMessage: 'Mute address is required.', lengthCode: 'mute_validation', - validationCode: 'mute_validation' + validationCode: 'mute_validation', + reflectMethod: 'setMuteState' } +} - constructor (deps = {}) { - super(deps) - this.profiles = deps.profiles - } - - // Validate a candidate cash address. Returns { ok: true } or throws a typed - // validation error. - validate (addr) { - if (typeof addr !== 'string' || addr.trim().length === 0) { - const err = new Error(this.emptyMessage) - err.code = this.validationCode - throw err - } - - try { - this._toHash160(addr) - return { ok: true } - } catch (err) { - const validationErr = new Error(`Invalid cash address: ${addr}`) - validationErr.code = this.validationCode - throw validationErr - } - } +class MemoMute extends MemoStateAction { + static config = muteConfig() // Broadcast a Memo mute for the given mutee address. async mute (muteeAddr) { - return this._broadcastAction(muteeAddr, MEMO_MUTE_PREFIX, true) + return this._setState(muteeAddr, MEMO_MUTE_PREFIX, true) } // Broadcast a Memo unmute for the given mutee address. async unmute (muteeAddr) { - return this._broadcastAction(muteeAddr, MEMO_UNMUTE_PREFIX, false) - } - - // Internal: validate, broadcast, and reflect a mute/unmute action. - async _broadcastAction (muteeAddr, prefix, isMute) { - if (!this.wallet) { - throw new Error(this.walletRequiredMsg) - } - this._ensureBchjs() - - this.validate(muteeAddr) - - await this.wallet.getUtxos() - - const hash160 = this._toHash160(muteeAddr) - const raw = Buffer.from(hash160, 'hex') - - const txid = await this.wallet.sendOpReturn(raw, prefix) - - this.reflect(txid, muteeAddr, isMute) - - return txid - } - - // Convert a cash address to its 20-byte hash160 hex string using the wallet's - // embedded bch-js. - _toHash160 (addr) { - return this.wallet.bchjs.Address.toHash160(addr) - } - - _ensureBchjs () { - if (!this.wallet.bchjs || typeof this.wallet.bchjs.Address.toHash160 !== 'function') { - throw new Error('Wallet does not expose bch-js Address.toHash160.') - } - } - - // Record the new mute state on the injected profile store when it exposes - // the mute methods. - reflect (txid, muteeAddr, isMute) { - if (this.profiles && typeof this.profiles.setMuteState === 'function') { - const myAddr = this.wallet?.walletInfo?.cashAddress - this.profiles.setMuteState(myAddr, muteeAddr, isMute) - } + return this._setState(muteeAddr, MEMO_UNMUTE_PREFIX, false) } } MemoMute.MEMO_MUTE_PREFIX = MEMO_MUTE_PREFIX MemoMute.MEMO_UNMUTE_PREFIX = MEMO_UNMUTE_PREFIX -MemoMute.PK_HASH_LENGTH = PK_HASH_LENGTH +MemoMute.PK_HASH_LENGTH = MemoStateAction.PK_HASH_LENGTH module.exports = MemoMute // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-29T03:07:20.508Z","module_hash":"33469aed44ef4e9225a9ea952c5cd9335f46a0b1c1298525b3fd51ae2f77f817","functions":[{"id":"func/MemoMute.constructor","name":"MemoMute.constructor","line":35,"end_line":38,"hash":"9407b43605444074011b1da595c9d53356352c72b0d847e00365d79ad705663a"},{"id":"func/MemoMute.validate","name":"MemoMute.validate","line":42,"end_line":57,"hash":"4154af442c36324ba147dff352c55610397fb430c9b62fbbe44728ab0e379179"},{"id":"func/MemoMute.mute","name":"MemoMute.mute","line":60,"end_line":62,"hash":"60306e96a5b6ef4bac8cf3fc516785880b2ccab835730019bbd4b9bbfa27e464"},{"id":"func/MemoMute.unmute","name":"MemoMute.unmute","line":65,"end_line":67,"hash":"f6476de3d8b62b3f5e864dc83d005452bc9842d9accfed2c64f57082e0fec7b3"},{"id":"func/MemoMute._broadcastAction","name":"MemoMute._broadcastAction","line":70,"end_line":88,"hash":"05f7860e1db59b7611027186c859d790f8d8c74b150daf3b687a8c9dc453f751"},{"id":"func/MemoMute._toHash160","name":"MemoMute._toHash160","line":92,"end_line":94,"hash":"3ed2ba4853b718a8316345c9f988ed3bed3ef647de3fd9761ee3c9e0bee86070"},{"id":"func/MemoMute._ensureBchjs","name":"MemoMute._ensureBchjs","line":96,"end_line":100,"hash":"92828d9038c9a4d367e80edd7f129291e434d57aae80b332081a66af9896a5d6"},{"id":"func/MemoMute.reflect","name":"MemoMute.reflect","line":104,"end_line":109,"hash":"6053ec62558bbc19bd5e55b19541a6fe5cde28fe6fbf7c4eede209652e835bc4"}]} +// {"version":1,"tested_at":"2026-09-04T18:27:53.801Z","module_hash":"05cb4f14173db9584533e58f931d775a2505d844f53bd29724935c3de083c40b","functions":[{"id":"func/muteConfig","name":"muteConfig","line":25,"end_line":35,"hash":"f05b5b03c441735322cdb433417ae5351626ed4c7d1db0a94cbf350f6818a605"},{"id":"func/MemoMute.mute","name":"MemoMute.mute","line":41,"end_line":43,"hash":"ad4c0ae3933e732017a59814bc6b9f6e7d263669f59440f1010209403ce0066f"},{"id":"func/MemoMute.unmute","name":"MemoMute.unmute","line":46,"end_line":48,"hash":"758b2676537ce5861f94ca9efff5b98a946bd82c023964e4d0c40f90d0a1261a"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-client/src/services/memo-state-action.js b/psf-memo-client/src/services/memo-state-action.js new file mode 100644 index 0000000..7c8cdc2 --- /dev/null +++ b/psf-memo-client/src/services/memo-state-action.js @@ -0,0 +1,100 @@ +/* + Shared base for Memo follow/unfollow and mute/unmute (state) actions. + + These actions all validate a cash address, convert it to its 20-byte + hash160, and broadcast an OP_RETURN carrying that hash160 payload with the + protocol prefix for a given state transition. On success they reflect the + new state on an injected profile store. + + Subclasses extend MemoAction's config with the state-specific messages and + codes, plus `reflectMethod` naming which profile store method records the + new state, and expose thin methods (follow/unfollow or mute/unmute) that + delegate to the shared `_setState` helper with the appropriate prefix. + + The wallet and an injected profile store are used so this module stays + testable and free of UI/network concerns; environmentally unsuitable I/O + lives behind those small adapter boundaries. +*/ + +const MemoAction = require('./memo-action') +const { hexToBytes } = require('./hex') + +const PK_HASH_LENGTH = 20 + +class MemoStateAction extends MemoAction { + constructor (deps = {}) { + super(deps) + this.profiles = deps.profiles + } + + // Validate a candidate cash address. Returns { ok: true } or throws a typed + // validation error. + validate (addr) { + if (typeof addr !== 'string' || addr.trim().length === 0) { + const err = new Error(this.emptyMessage) + err.code = this.validationCode + throw err + } + + try { + this._toHash160(addr) + return { ok: true } + } catch (err) { + const validationErr = new Error(`Invalid cash address: ${addr}`) + validationErr.code = this.validationCode + throw validationErr + } + } + + // Internal: validate, broadcast, and reflect a state transition. Subclasses + // delegate their public methods here with the transition's prefix. + async _setState (targetAddr, prefix, state) { + if (!this.wallet) { + throw new Error(this.walletRequiredMsg) + } + this._ensureBchjs() + + this.validate(targetAddr) + + await this.wallet.getUtxos() + + const hash160 = this._toHash160(targetAddr) + const raw = hexToBytes(hash160, PK_HASH_LENGTH, 'Address hash160') + + const txid = await this.wallet.sendOpReturn(raw, prefix) + + this.reflect(txid, targetAddr, state) + + return txid + } + + // Convert a cash address to its 20-byte hash160 hex string using the wallet's + // embedded bch-js. + _toHash160 (addr) { + return this.wallet.bchjs.Address.toHash160(addr) + } + + _ensureBchjs () { + if (!this.wallet.bchjs || typeof this.wallet.bchjs.Address.toHash160 !== 'function') { + throw new Error('Wallet does not expose bch-js Address.toHash160.') + } + } + + // Record the new state on the injected profile store method named by the + // subclass config. + reflect (txid, targetAddr, state) { + const reflectMethod = this.constructor.config.reflectMethod + if (this.profiles && typeof this.profiles[reflectMethod] === 'function') { + const myAddr = this.wallet?.walletInfo?.cashAddress + this.profiles[reflectMethod](myAddr, targetAddr, state) + } + } +} + +MemoStateAction.PK_HASH_LENGTH = PK_HASH_LENGTH + +module.exports = MemoStateAction + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-04T18:28:28.750Z","module_hash":"938e453562e0258b383cbc31f449fccc2e6105c805c97c52fb13bef3ee0fe6b4","functions":[{"id":"func/MemoStateAction.constructor","name":"MemoStateAction.constructor","line":25,"end_line":28,"hash":"9407b43605444074011b1da595c9d53356352c72b0d847e00365d79ad705663a"},{"id":"func/MemoStateAction.validate","name":"MemoStateAction.validate","line":32,"end_line":47,"hash":"4154af442c36324ba147dff352c55610397fb430c9b62fbbe44728ab0e379179"},{"id":"func/MemoStateAction._setState","name":"MemoStateAction._setState","line":51,"end_line":69,"hash":"86b976bcbe8532f7bfb9bdc989976f2266bce3d0e8c3a77a1f3e0b8413951684"},{"id":"func/MemoStateAction._toHash160","name":"MemoStateAction._toHash160","line":73,"end_line":75,"hash":"3ed2ba4853b718a8316345c9f988ed3bed3ef647de3fd9761ee3c9e0bee86070"},{"id":"func/MemoStateAction._ensureBchjs","name":"MemoStateAction._ensureBchjs","line":77,"end_line":81,"hash":"92828d9038c9a4d367e80edd7f129291e434d57aae80b332081a66af9896a5d6"},{"id":"func/MemoStateAction.reflect","name":"MemoStateAction.reflect","line":85,"end_line":91,"hash":"4ef0ab37e0ab08a681475805c5e53a010b35aa56560bc84f6d2189154ed7fa7d"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-client/test/property/follow.property.test.js b/psf-memo-client/test/property/follow.property.test.js index b795192..90000d3 100644 --- a/psf-memo-client/test/property/follow.property.test.js +++ b/psf-memo-client/test/property/follow.property.test.js @@ -24,6 +24,10 @@ const rng = seededRandom(20260830) const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' const CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' +function broadcastHex (wallet, index = 0) { + return Buffer.from(wallet.broadcasts[index].msg).toString('hex') +} + // Deterministic 20-byte hash160 hex for any input string, mirroring what a // real wallet's bch-js produces for a valid cash address. function hash20 (s) { @@ -86,9 +90,9 @@ test('follow broadcasts exactly one hash160 payload with the follow prefix', asy return wallet.broadcasts.length === 1 && wallet.broadcasts[0].prefix === MemoFollow.MEMO_FOLLOW_PREFIX && - Buffer.isBuffer(wallet.broadcasts[0].msg) && + wallet.broadcasts[0].msg instanceof Uint8Array && wallet.broadcasts[0].msg.length === MemoFollow.PK_HASH_LENGTH && - wallet.broadcasts[0].msg.toString('hex') === hash20(addr) + broadcastHex(wallet, 0) === hash20(addr) }, { label: 'follow broadcast conservation and hash160 length' } ) @@ -104,9 +108,9 @@ test('unfollow broadcasts exactly one hash160 payload with the unfollow prefix', return wallet.broadcasts.length === 1 && wallet.broadcasts[0].prefix === MemoFollow.MEMO_UNFOLLOW_PREFIX && - Buffer.isBuffer(wallet.broadcasts[0].msg) && + wallet.broadcasts[0].msg instanceof Uint8Array && wallet.broadcasts[0].msg.length === MemoFollow.PK_HASH_LENGTH && - wallet.broadcasts[0].msg.toString('hex') === hash20(addr) + broadcastHex(wallet, 0) === hash20(addr) }, { label: 'unfollow broadcast conservation and hash160 length' } ) diff --git a/psf-memo-client/test/property/mute-services.property.test.js b/psf-memo-client/test/property/mute-services.property.test.js index 65f6f3e..e9b557d 100644 --- a/psf-memo-client/test/property/mute-services.property.test.js +++ b/psf-memo-client/test/property/mute-services.property.test.js @@ -24,6 +24,10 @@ const rng = seededRandom(20260830) const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' const CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' +function broadcastHex (wallet, index = 0) { + return Buffer.from(wallet.broadcasts[index].msg).toString('hex') +} + // Deterministic 20-byte hash160 hex for any input string, mirroring what a // real wallet's bch-js produces for a valid cash address. function hash20 (s) { @@ -86,9 +90,9 @@ test('mute broadcasts exactly one hash160 payload with the mute prefix', async ( return wallet.broadcasts.length === 1 && wallet.broadcasts[0].prefix === MemoMute.MEMO_MUTE_PREFIX && - Buffer.isBuffer(wallet.broadcasts[0].msg) && + wallet.broadcasts[0].msg instanceof Uint8Array && wallet.broadcasts[0].msg.length === MemoMute.PK_HASH_LENGTH && - wallet.broadcasts[0].msg.toString('hex') === hash20(addr) + broadcastHex(wallet, 0) === hash20(addr) }, { label: 'mute broadcast conservation and hash160 length' } ) @@ -104,9 +108,9 @@ test('unmute broadcasts exactly one hash160 payload with the unmute prefix', asy return wallet.broadcasts.length === 1 && wallet.broadcasts[0].prefix === MemoMute.MEMO_UNMUTE_PREFIX && - Buffer.isBuffer(wallet.broadcasts[0].msg) && + wallet.broadcasts[0].msg instanceof Uint8Array && wallet.broadcasts[0].msg.length === MemoMute.PK_HASH_LENGTH && - wallet.broadcasts[0].msg.toString('hex') === hash20(addr) + broadcastHex(wallet, 0) === hash20(addr) }, { label: 'unmute broadcast conservation and hash160 length' } ) diff --git a/psf-memo-client/test/unit/memo-follow.test.js b/psf-memo-client/test/unit/memo-follow.test.js index 216b07c..4c73272 100644 --- a/psf-memo-client/test/unit/memo-follow.test.js +++ b/psf-memo-client/test/unit/memo-follow.test.js @@ -17,6 +17,10 @@ const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' const FOLLOWEE_ADDRESS = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' const FOLLOWEE_HASH160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' +function broadcastHex (wallet, index = 0) { + return Buffer.from(wallet.broadcasts[index].msg).toString('hex') +} + function makeBchjs () { return { Address: { @@ -63,8 +67,8 @@ test('follow broadcasts with the Memo follow prefix and hash160 payload', async assert.equal(wallet.broadcasts.length, 1) assert.equal(wallet.broadcasts[0].prefix, MemoFollow.MEMO_FOLLOW_PREFIX) - assert.ok(Buffer.isBuffer(wallet.broadcasts[0].msg)) - assert.equal(wallet.broadcasts[0].msg.toString('hex'), FOLLOWEE_HASH160) + assert.equal(wallet.broadcasts[0].msg.length, MemoFollow.PK_HASH_LENGTH) + assert.equal(broadcastHex(wallet, 0), FOLLOWEE_HASH160) }) test('unfollow broadcasts with the Memo unfollow prefix and hash160 payload', async () => { @@ -75,7 +79,8 @@ test('unfollow broadcasts with the Memo unfollow prefix and hash160 payload', as assert.equal(wallet.broadcasts.length, 1) assert.equal(wallet.broadcasts[0].prefix, MemoFollow.MEMO_UNFOLLOW_PREFIX) - assert.equal(wallet.broadcasts[0].msg.toString('hex'), FOLLOWEE_HASH160) + assert.equal(wallet.broadcasts[0].msg.length, MemoFollow.PK_HASH_LENGTH) + assert.equal(broadcastHex(wallet, 0), FOLLOWEE_HASH160) }) test('follow reflects the new follow state on the profile store', async () => { diff --git a/psf-memo-client/test/unit/memo-mute.test.js b/psf-memo-client/test/unit/memo-mute.test.js index 896f70e..7553724 100644 --- a/psf-memo-client/test/unit/memo-mute.test.js +++ b/psf-memo-client/test/unit/memo-mute.test.js @@ -12,6 +12,10 @@ const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' const MUTEE_ADDRESS = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' const MUTEE_HASH160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' +function broadcastHex (wallet, index = 0) { + return Buffer.from(wallet.broadcasts[index].msg).toString('hex') +} + function makeBchjs () { return { Address: { @@ -58,8 +62,8 @@ test('mute broadcasts with the Memo mute prefix and hash160 payload', async () = assert.equal(wallet.broadcasts.length, 1) assert.equal(wallet.broadcasts[0].prefix, MemoMute.MEMO_MUTE_PREFIX) - assert.ok(Buffer.isBuffer(wallet.broadcasts[0].msg)) - assert.equal(wallet.broadcasts[0].msg.toString('hex'), MUTEE_HASH160) + assert.equal(wallet.broadcasts[0].msg.length, MemoMute.PK_HASH_LENGTH) + assert.equal(broadcastHex(wallet, 0), MUTEE_HASH160) }) test('unmute broadcasts with the Memo unmute prefix and hash160 payload', async () => { @@ -70,7 +74,8 @@ test('unmute broadcasts with the Memo unmute prefix and hash160 payload', async assert.equal(wallet.broadcasts.length, 1) assert.equal(wallet.broadcasts[0].prefix, MemoMute.MEMO_UNMUTE_PREFIX) - assert.equal(wallet.broadcasts[0].msg.toString('hex'), MUTEE_HASH160) + assert.equal(wallet.broadcasts[0].msg.length, MemoMute.PK_HASH_LENGTH) + assert.equal(broadcastHex(wallet, 0), MUTEE_HASH160) }) test('mute reflects the new mute state on the profile store', async () => {