mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-23 01:32:00 -07:00
Deduplicate Memo follow/mute onto shared MemoStateAction base
Extract the near-identical follow/unfollow and mute/unmute action classes onto a config-driven MemoStateAction base, removing 150 duplicated lines. Preserves public API and static exports. Coder's hexToBytes broadcast stays intact. Refreshed mutation manifests via the tool. By refactorer.
This commit is contained in:
@@ -16,106 +16,44 @@
|
||||
PK_HASH_LENGTH : size of the followee hash160 in bytes (20)
|
||||
*/
|
||||
|
||||
const MemoAction = require('./memo-action')
|
||||
const { hexToBytes } = require('./hex')
|
||||
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 = hexToBytes(hash160, PK_HASH_LENGTH, 'Address hash160')
|
||||
|
||||
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:22:54.857Z","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
|
||||
|
||||
@@ -16,106 +16,44 @@
|
||||
PK_HASH_LENGTH : size of the mutee hash160 in bytes (20)
|
||||
*/
|
||||
|
||||
const MemoAction = require('./memo-action')
|
||||
const { hexToBytes } = require('./hex')
|
||||
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 = hexToBytes(hash160, PK_HASH_LENGTH, 'Address hash160')
|
||||
|
||||
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:22:55.701Z","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:22:56.557Z","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
|
||||
Reference in New Issue
Block a user