Merge architect branch for binary-payload-broadcast

Follow/mute/unfollow/unmute now broadcast the target's raw 20-byte hash160
as the OP_RETURN payload via a browser-safe Uint8Array (no Node Buffer
global), shared through the new MemoStateAction base.

By specifier.
This commit is contained in:
Chris Troutner
2026-09-04 11:31:54 -07:00
10 changed files with 284 additions and 164 deletions
@@ -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.
+38 -2
View File
@@ -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?$/,
@@ -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
+13 -74
View File
@@ -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
+13 -74
View File
@@ -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
@@ -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
@@ -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' }
)
@@ -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' }
)
@@ -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 () => {
+8 -3
View File
@@ -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 () => {