Review multi-push encoding: fix browser Buffer and harden coverage

Import the browser-safe buffer module in the multi-push adapter so the
CRA production bundle no longer reads the Node global Buffer, add
regression tests for the falsy-wallet and idempotent-attach paths, share
the script-encoding test helper, and de-duplicate the UTF-8 push
acceptance assertions. Refresh the language mutation manifests and the
soft acceptance-mutation manifest.

By architect.
This commit is contained in:
Chris Troutner
2026-09-16 21:31:23 -07:00
parent 8310b6a1d8
commit fac01730c4
16 changed files with 89 additions and 48 deletions
+13 -15
View File
@@ -589,6 +589,16 @@ function txidWireHex (txid) {
return Buffer.from(txid, 'hex').reverse().toString('hex') return Buffer.from(txid, 'hex').reverse().toString('hex')
} }
// Assert that the most recent broadcast's push at `index` decodes to the
// expected UTF-8 text. Shared by the topic/text/question push assertions.
function assertUtf8Push (world, index, expected, label) {
const push = broadcastPush(world, index)
const actual = push.toString('utf8')
if (actual !== expected) {
throw new Error(`${label} push "${actual}" did not match "${expected}".`)
}
}
// Resolve a literal value or a <parameter> placeholder from the example store. // Resolve a literal value or a <parameter> placeholder from the example store.
function resolveParam (value, example) { function resolveParam (value, example) {
const match = /^<([A-Za-z0-9_]+)>$/.exec(String(value).trim()) const match = /^<([A-Za-z0-9_]+)>$/.exec(String(value).trim())
@@ -1113,11 +1123,7 @@ const handlers = [
name: 'second broadcast push is UTF-8 topic', name: 'second broadcast push is UTF-8 topic',
pattern: /^the second broadcast push is the UTF-8 topic "(.+)"$/, pattern: /^the second broadcast push is the UTF-8 topic "(.+)"$/,
run (m, example, world) { run (m, example, world) {
const topic = resolveText(m[1], example) assertUtf8Push(world, 1, resolveText(m[1], example), 'Second')
const push = broadcastPush(world, 1)
if (push.toString('utf8') !== topic) {
throw new Error(`Second push "${push.toString('utf8')}" did not match topic "${topic}".`)
}
} }
}, },
{ {
@@ -1135,11 +1141,7 @@ const handlers = [
name: 'third broadcast push is UTF-8 text', name: 'third broadcast push is UTF-8 text',
pattern: /^the third broadcast push is the UTF-8 text "(.+)"$/, pattern: /^the third broadcast push is the UTF-8 text "(.+)"$/,
run (m, example, world) { run (m, example, world) {
const text = resolveText(m[1], example) assertUtf8Push(world, 2, resolveText(m[1], example), 'Third')
const push = broadcastPush(world, 2)
if (push.toString('utf8') !== text) {
throw new Error(`Third push "${push.toString('utf8')}" did not match text "${text}".`)
}
} }
}, },
{ {
@@ -1157,11 +1159,7 @@ const handlers = [
name: 'fourth broadcast push is UTF-8 question', name: 'fourth broadcast push is UTF-8 question',
pattern: /^the fourth broadcast push is the UTF-8 question "(.+)"$/, pattern: /^the fourth broadcast push is the UTF-8 question "(.+)"$/,
run (m, example, world) { run (m, example, world) {
const question = resolveText(m[1], example) assertUtf8Push(world, 3, resolveText(m[1], example), 'Fourth')
const push = broadcastPush(world, 3)
if (push.toString('utf8') !== question) {
throw new Error(`Fourth push "${push.toString('utf8')}" did not match question "${question}".`)
}
} }
}, },
{ {
+3 -3
View File
@@ -17,6 +17,7 @@
"bch-message-lib": "2.2.1", "bch-message-lib": "2.2.1",
"bch-token-sweep": "2.2.1", "bch-token-sweep": "2.2.1",
"bootstrap": "5.2.0", "bootstrap": "5.2.0",
"buffer": "^6.0.3",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"query-string": "7.1.1", "query-string": "7.1.1",
"react": "19.0.0", "react": "19.0.0",
@@ -6160,7 +6161,6 @@
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -6637,7 +6637,8 @@
}, },
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"dev": true, "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -11703,7 +11704,6 @@
}, },
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
+1
View File
@@ -11,6 +11,7 @@
"bch-message-lib": "2.2.1", "bch-message-lib": "2.2.1",
"bch-token-sweep": "2.2.1", "bch-token-sweep": "2.2.1",
"bootstrap": "5.2.0", "bootstrap": "5.2.0",
"buffer": "^6.0.3",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"query-string": "7.1.1", "query-string": "7.1.1",
"react": "19.0.0", "react": "19.0.0",
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-09-17T04:24:27.366008662Z","feature_name":"Memo multi-push encoding","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/memo-multipush-encoding.feature","background_hash":"e1d5f81f1ed083ac6934c429ca3cb4a0f8d4dac44c2eaa45c0960920bde2c017","implementation_hash":"unknown","scenarios":[]}
# acceptance-mutation-manifest-end
# Scenarios: Memo multi-push encoding - 1, Memo multi-push encoding - 2, Memo multi-push encoding - 3, Memo multi-push encoding - 4, Memo multi-push encoding - 5 # Scenarios: Memo multi-push encoding - 1, Memo multi-push encoding - 2, Memo multi-push encoding - 3, Memo multi-push encoding - 4, Memo multi-push encoding - 5
# #
# Memo actions that carry more than one field encode each protocol field as # Memo actions that carry more than one field encode each protocol field as
+1 -1
View File
@@ -47,5 +47,5 @@ function buildTxidTextPushes (txid, text, label = 'Poll txid') {
module.exports = { hexToBytes, txidToWireBytes, buildTxidTextPushes } module.exports = { hexToBytes, txidToWireBytes, buildTxidTextPushes }
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-16T22:01:27.280Z","module_hash":"454b45f3ce08c5ea6346c113db6384f7a44dddbd21e2fb0a35c41ce6701e62c8","functions":[{"id":"func/hexToBytes","name":"hexToBytes","line":12,"end_line":26,"hash":"dbf7e0a434598f85365f5a60a0ca227a17a1bcd6718a78bb5aaf7afb8eb2487b"},{"id":"func/txidToWireBytes","name":"txidToWireBytes","line":32,"end_line":34,"hash":"ac25753fecb32c929134b6e6379cae0bfddd4aa6f7ae438a08da650450e9ea1a"},{"id":"func/buildTxidTextPayload","name":"buildTxidTextPayload","line":39,"end_line":46,"hash":"068504ea13a037adabbbfeab122fb5ec6113625a9e0b009cbbe8b46b6bebdaee"}]} // {"version":1,"tested_at":"2026-09-17T04:10:11.935Z","module_hash":"e3a608fad7d52d78c2c94a873161d6085c9afc2326a07fa04eb8a67815ed362a","functions":[{"id":"func/hexToBytes","name":"hexToBytes","line":14,"end_line":28,"hash":"dbf7e0a434598f85365f5a60a0ca227a17a1bcd6718a78bb5aaf7afb8eb2487b"},{"id":"func/txidToWireBytes","name":"txidToWireBytes","line":34,"end_line":36,"hash":"ac25753fecb32c929134b6e6379cae0bfddd4aa6f7ae438a08da650450e9ea1a"},{"id":"func/buildTxidTextPushes","name":"buildTxidTextPushes","line":41,"end_line":45,"hash":"815887680e60a7ed823b15fe6cc25b16f931e4390e37dd89aee409cf55836fd0"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
@@ -15,6 +15,13 @@
the wallet wiring is the small environmentally unsuitable boundary. the wallet wiring is the small environmentally unsuitable boundary.
*/ */
// Import the browser-compatible Buffer implementation. `Buffer` is a Node
// global, but the CRA production bundle does not polyfill Node globals and the
// wallet script does not define window.Buffer, so a bare global would throw in
// the browser. The npm buffer package is the same implementation the bundled
// bitcoincashjs-lib uses, so its Buffer.isBuffer accepts these pushes.
const { Buffer } = require('buffer')
// Normalize one push value (a UTF-8 string or byte array) to a Buffer. // Normalize one push value (a UTF-8 string or byte array) to a Buffer.
function toPushBuffer (value) { function toPushBuffer (value) {
if (typeof value === 'string') return Buffer.from(value, 'utf8') if (typeof value === 'string') return Buffer.from(value, 'utf8')
@@ -86,3 +93,7 @@ module.exports = {
broadcastMultiPush, broadcastMultiPush,
attachMultiPushOpReturn attachMultiPushOpReturn
} }
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-17T04:08:42.622Z","module_hash":"9bce63e69187b0a42ba210437ea14a32fdd7f53ccfd905264375bba997a3a4c3","functions":[{"id":"func/toPushBuffer","name":"toPushBuffer","line":26,"end_line":29,"hash":"0ff6744e881d65ce60d6456df450f5f0f26a39492d387b2b33aefec8c12e2e81"},{"id":"func/buildPushes","name":"buildPushes","line":33,"end_line":35,"hash":"46961294949a914dbab5064b1b2fcf5c3ba59c2e438b2a986275d3dfed40ea71"},{"id":"func/broadcastMultiPush","name":"broadcastMultiPush","line":40,"end_line":73,"hash":"ee5022606ea152763159d7fd9753c3849b723c5a8ab1cd1cb399feb4248479d6"},{"id":"func/attachMultiPushOpReturn","name":"attachMultiPushOpReturn","line":77,"end_line":88,"hash":"ef81a1526f376fc1ad500d72dafdd684d78ad8052202e938a39654bb182e8eeb"}]}
// mutate4javascript-manifest-end
@@ -102,5 +102,5 @@ MemoPollCreate.DEFAULT_POLL_TYPE = DEFAULT_POLL_TYPE
module.exports = MemoPollCreate module.exports = MemoPollCreate
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T22:45:49.496Z","module_hash":"3455852bee199b40530bbbe787f85429a7a15bf0116989231ac06f9ca9eadf36","functions":[{"id":"func/MemoPollCreate.constructor","name":"MemoPollCreate.constructor","line":36,"end_line":40,"hash":"9404cf1f0df5cc8756e0ea24abab27ade844fa461f84818162faac807b838dd1"},{"id":"func/MemoPollCreate.isTooLong","name":"MemoPollCreate.isTooLong","line":43,"end_line":45,"hash":"ca9087a454f1fba64ac35738a037372ade36ba6d4c2270d2025adc00b860412c"},{"id":"func/MemoPollCreate.create","name":"MemoPollCreate.create","line":48,"end_line":71,"hash":"b5e769b07f3795a5db0d4202106f4b25b5a0b69e30e70491a7a9c2ac52085fce"},{"id":"func/MemoPollCreate.reflect","name":"MemoPollCreate.reflect","line":74,"end_line":84,"hash":"ec559b778b279c1efb46bdb2440a0c197dae242052d7c1ebf5331078258b719f"},{"id":"func/buildCreatePollPayload","name":"buildCreatePollPayload","line":89,"end_line":96,"hash":"f5abdaad00f9e3c857c65766a981d6971e1bc7c64e0200cbb4105ab798bd94da"}]} // {"version":1,"tested_at":"2026-09-17T04:15:33.060Z","module_hash":"f768a640a3cf8aff7dd9836bcbed38ca2d2fa6892cca97f8d078f4ad5d9d5287","functions":[{"id":"func/MemoPollCreate.constructor","name":"MemoPollCreate.constructor","line":36,"end_line":40,"hash":"9404cf1f0df5cc8756e0ea24abab27ade844fa461f84818162faac807b838dd1"},{"id":"func/MemoPollCreate.isTooLong","name":"MemoPollCreate.isTooLong","line":43,"end_line":45,"hash":"ca9087a454f1fba64ac35738a037372ade36ba6d4c2270d2025adc00b860412c"},{"id":"func/MemoPollCreate.create","name":"MemoPollCreate.create","line":48,"end_line":71,"hash":"dd0f77edcf6d20b5273c4949a0b7b52f7b60686556b33491139077bbb6866120"},{"id":"func/MemoPollCreate.reflect","name":"MemoPollCreate.reflect","line":74,"end_line":84,"hash":"ec559b778b279c1efb46bdb2440a0c197dae242052d7c1ebf5331078258b719f"},{"id":"func/buildCreatePollPushes","name":"buildCreatePollPushes","line":89,"end_line":96,"hash":"54b38bb7fba8a90968c93441588aa42754d2b961d75778daf8b764a017030334"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
@@ -45,5 +45,5 @@ MemoPollOption.MAX_OPTION_BYTES = MAX_OPTION_BYTES
module.exports = MemoPollOption module.exports = MemoPollOption
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T23:38:02.285Z","module_hash":"f8432d9ccb0073f5cce9f96415fdfb033876f498a7d80aadaca1ed96d89d4ce3","functions":[{"id":"func/MemoPollOption.add","name":"MemoPollOption.add","line":37,"end_line":39,"hash":"e0343ddce138d48e3fec2c5c0169e9d99f374835cc95d381383a254297d7f1dc"}]} // {"version":1,"tested_at":"2026-09-17T04:16:42.577Z","module_hash":"4bcfeb64bcefaafb3d61272db2623e832b65930f966f5494bebfd417b29dc833","functions":[{"id":"func/MemoPollOption.add","name":"MemoPollOption.add","line":37,"end_line":39,"hash":"d8bdf2dda07743c5d6927b7a6ef69715f033aefe2e77b5ca6923c2afe3772088"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
@@ -45,5 +45,5 @@ MemoPollVote.MAX_COMMENT_BYTES = MAX_COMMENT_BYTES
module.exports = MemoPollVote module.exports = MemoPollVote
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T23:38:23.086Z","module_hash":"c60989293dede6eeaf8edd168d00745e7299620b80874f87680337cc2f4d36fc","functions":[{"id":"func/MemoPollVote.vote","name":"MemoPollVote.vote","line":37,"end_line":39,"hash":"c6c194dfaf6c92f5cafcf86772bd295d038536ea20079931e4144eb5141c7101"}]} // {"version":1,"tested_at":"2026-09-17T04:17:10.193Z","module_hash":"e03a12dfad981e8a9ce4d4af763f0c57bd7a0f70a8bf22adc4b83d539b4e4725","functions":[{"id":"func/MemoPollVote.vote","name":"MemoPollVote.vote","line":37,"end_line":39,"hash":"e7ec8ae8f66c762cd7bb9fd9bb3001fe2a1b63843933d57372d6fd6f6a87b3dc"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
+1 -1
View File
@@ -85,5 +85,5 @@ MemoReply.MAX_REPLY_BYTES = MAX_REPLY_BYTES
module.exports = MemoReply module.exports = MemoReply
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-16T22:14:19.213Z","module_hash":"6c515a91afe94a6be8671a1541302446bb6b1015d03ce0e278644508ebd20090","functions":[{"id":"func/MemoReply.constructor","name":"MemoReply.constructor","line":36,"end_line":39,"hash":"23091c1b8f7847199bab3b54d8d81e9d8432c6da96138d72ac03fcf9426d542c"},{"id":"func/MemoReply.isTooLong","name":"MemoReply.isTooLong","line":42,"end_line":44,"hash":"2e867501d184010313ba9b27a6bb1e446df8f093514ee231b90ce77699ecbaf2"},{"id":"func/MemoReply.reply","name":"MemoReply.reply","line":48,"end_line":67,"hash":"00fb3598d5c3bdc3c6744d1316ad552364574fed105438af4aa011347f3306ce"},{"id":"func/MemoReply.reflect","name":"MemoReply.reflect","line":70,"end_line":79,"hash":"344e1bf304a4dfd475b02824b7ddbf555da0f3ec89f73b4f6009cf0bf097fb02"}]} // {"version":1,"tested_at":"2026-09-17T04:12:17.098Z","module_hash":"53a16ce9198c0c94446ca0f89463072d83a9646de676708a1b9061086dbac40c","functions":[{"id":"func/MemoReply.constructor","name":"MemoReply.constructor","line":36,"end_line":39,"hash":"23091c1b8f7847199bab3b54d8d81e9d8432c6da96138d72ac03fcf9426d542c"},{"id":"func/MemoReply.isTooLong","name":"MemoReply.isTooLong","line":42,"end_line":44,"hash":"2e867501d184010313ba9b27a6bb1e446df8f093514ee231b90ce77699ecbaf2"},{"id":"func/MemoReply.reply","name":"MemoReply.reply","line":48,"end_line":67,"hash":"b8dc4228aefdae7a9e29273a7cef7d92177af9ba6d19d318d6455bf4d202fcc6"},{"id":"func/MemoReply.reflect","name":"MemoReply.reflect","line":70,"end_line":79,"hash":"344e1bf304a4dfd475b02824b7ddbf555da0f3ec89f73b4f6009cf0bf097fb02"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
@@ -88,5 +88,5 @@ MemoTopicPost.MAX_TOPIC_MESSAGE_BYTES = MAX_TOPIC_MESSAGE_BYTES
module.exports = MemoTopicPost module.exports = MemoTopicPost
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T18:18:11.190Z","module_hash":"c73b1395983accf085921554a84c8ef9ad8014ba8d8ddfa794e051ae4a487f04","functions":[{"id":"func/MemoTopicPost.constructor","name":"MemoTopicPost.constructor","line":34,"end_line":38,"hash":"34483e7579cbde3babf17cc43f64bc6535537b6391169a75cb51025029283289"},{"id":"func/MemoTopicPost.isTooLong","name":"MemoTopicPost.isTooLong","line":42,"end_line":44,"hash":"a8d16816cf0aeda1be759f796aee75e9cc5d89ccbcc288b5d2551917121be27a"},{"id":"func/MemoTopicPost.remainingBytes","name":"MemoTopicPost.remainingBytes","line":46,"end_line":48,"hash":"11a07f6539fa6b74feabfdd258f83ede4556213951aa85266a959871ff616df1"},{"id":"func/MemoTopicPost.post","name":"MemoTopicPost.post","line":51,"end_line":67,"hash":"dec3508e4969efd3edfefbc0752ca4608bac9521fbda62cf2ce5d7bf0a5696c7"},{"id":"func/MemoTopicPost.reflect","name":"MemoTopicPost.reflect","line":70,"end_line":79,"hash":"8a136fe98fa6752b2247b9bc0064b9860cfa34e518abaa33e50e95833120d529"}]} // {"version":1,"tested_at":"2026-09-17T04:13:34.090Z","module_hash":"c718cc342a8316ce9369d79969492e7de83e2c8b9d881c2f3d0324ac92cdf0ff","functions":[{"id":"func/MemoTopicPost.constructor","name":"MemoTopicPost.constructor","line":34,"end_line":38,"hash":"34483e7579cbde3babf17cc43f64bc6535537b6391169a75cb51025029283289"},{"id":"func/MemoTopicPost.isTooLong","name":"MemoTopicPost.isTooLong","line":42,"end_line":44,"hash":"a8d16816cf0aeda1be759f796aee75e9cc5d89ccbcc288b5d2551917121be27a"},{"id":"func/MemoTopicPost.remainingBytes","name":"MemoTopicPost.remainingBytes","line":46,"end_line":48,"hash":"11a07f6539fa6b74feabfdd258f83ede4556213951aa85266a959871ff616df1"},{"id":"func/MemoTopicPost.post","name":"MemoTopicPost.post","line":51,"end_line":70,"hash":"626b580f6c5192b03254561430a28b04decf8d089fdb12542bea8a168803e945"},{"id":"func/MemoTopicPost.reflect","name":"MemoTopicPost.reflect","line":73,"end_line":82,"hash":"8a136fe98fa6752b2247b9bc0064b9860cfa34e518abaa33e50e95833120d529"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
@@ -73,5 +73,5 @@ class MemoTxidAction extends MemoAction {
module.exports = MemoTxidAction module.exports = MemoTxidAction
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T23:37:21.781Z","module_hash":"b43e695aee6198c465b2cdbb0fbb405fccb0aadb573f56c3d3249de3920c1939","functions":[{"id":"func/MemoTxidAction.constructor","name":"MemoTxidAction.constructor","line":18,"end_line":22,"hash":"399be38855508b45b6a07e00361d4fb36186df3fdac5e2a68e004f7dcbdd353c"},{"id":"func/MemoTxidAction.broadcastTxid","name":"MemoTxidAction.broadcastTxid","line":26,"end_line":48,"hash":"395ea932dd691049556a66a3d4c674778818b8b53152c0ae7088d960524fc400"},{"id":"func/MemoTxidAction.reflect","name":"MemoTxidAction.reflect","line":53,"end_line":63,"hash":"7d61638162b7142298e021b02cbdeaca359f33503b3b84a308498f0479c80c5c"},{"id":"func/MemoTxidAction.isTooLong","name":"MemoTxidAction.isTooLong","line":67,"end_line":69,"hash":"7eeb0b9dcb35530de412b0aa0ade6b267e2fe22f5e49aa0f3ecf5b9124022a3b"}]} // {"version":1,"tested_at":"2026-09-17T04:18:05.479Z","module_hash":"d0bda2cafd87fb912744a896f53c74aac9402bc087e106eccbf19144b9f93e35","functions":[{"id":"func/MemoTxidAction.constructor","name":"MemoTxidAction.constructor","line":18,"end_line":22,"hash":"399be38855508b45b6a07e00361d4fb36186df3fdac5e2a68e004f7dcbdd353c"},{"id":"func/MemoTxidAction.broadcastTxid","name":"MemoTxidAction.broadcastTxid","line":27,"end_line":49,"hash":"60e57cd10e406285825ad6750d9cce312ea5ecb09e02a697da358ac65f8312a5"},{"id":"func/MemoTxidAction.reflect","name":"MemoTxidAction.reflect","line":54,"end_line":64,"hash":"7d61638162b7142298e021b02cbdeaca359f33503b3b84a308498f0479c80c5c"},{"id":"func/MemoTxidAction.isTooLong","name":"MemoTxidAction.isTooLong","line":68,"end_line":70,"hash":"7eeb0b9dcb35530de412b0aa0ade6b267e2fe22f5e49aa0f3ecf5b9124022a3b"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
+1 -1
View File
@@ -21,5 +21,5 @@ function byteLength (str) {
module.exports = { encodeUtf8, byteLength } module.exports = { encodeUtf8, byteLength }
// mutate4javascript-manifest-begin // mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-26T12:17:55.885Z","module_hash":"7f91541c49f2b6f421e8d4158bfe808b35a5449534cb26c567162fce6fec64bf","functions":[{"id":"func/byteLength","name":"byteLength","line":11,"end_line":13,"hash":"973c9dadcd1d8bbd53587252443880db13c8be3639fa74ce3c69a08ea358c8e2"}]} // {"version":1,"tested_at":"2026-09-17T04:11:15.796Z","module_hash":"530eeae383fa202f8e50d83126a9bafaaa49b04268f297e9e91027a89cde71b4","functions":[{"id":"func/encodeUtf8","name":"encodeUtf8","line":12,"end_line":14,"hash":"8d56bdd5607b741578d5f51511391e0376cc410181401b2570fde0990b6bf976"},{"id":"func/byteLength","name":"byteLength","line":17,"end_line":19,"hash":"4d5266d6b3715a761b533e67477846156cfbd497460e5af448a8ea7a72bb580c"}]}
// mutate4javascript-manifest-end // mutate4javascript-manifest-end
@@ -24,6 +24,7 @@ const {
buildPushes, buildPushes,
attachMultiPushOpReturn attachMultiPushOpReturn
} = require('../../src/services/memo-multipush') } = require('../../src/services/memo-multipush')
const { encodeScript } = require('../support/script-encoding')
const rng = seededRandom(20260916) const rng = seededRandom(20260916)
@@ -61,17 +62,6 @@ function randomFields () {
return fields return fields
} }
// Encode a script the way Bitcoin does: an opcode number is one byte, a
// Buffer becomes a length-prefixed push.
function encodeScript (script) {
const parts = script.map((el) => {
if (typeof el === 'number') return Buffer.from([el])
const buf = Buffer.from(el)
return Buffer.concat([Buffer.from([buf.length]), buf])
})
return Buffer.concat(parts)
}
// A double of the minimal-slp-wallet surface the adapter reuses. // A double of the minimal-slp-wallet surface the adapter reuses.
function makeWalletDouble () { function makeWalletDouble () {
const sent = [] const sent = []
@@ -0,0 +1,20 @@
/*
Test helper: encode a script the way Bitcoin does.
An opcode number is one byte; a Buffer/string becomes a length-prefixed push.
Enough to observe that the multi-push adapter produced the expected pushes.
Shared by the memo-multipush unit and property tests.
*/
'use strict'
function encodeScript (script) {
const parts = script.map((el) => {
if (typeof el === 'number') return Buffer.from([el])
const buf = Buffer.from(el)
return Buffer.concat([Buffer.from([buf.length]), buf])
})
return Buffer.concat(parts)
}
module.exports = { encodeScript }
@@ -18,17 +18,7 @@ const {
broadcastMultiPush, broadcastMultiPush,
attachMultiPushOpReturn attachMultiPushOpReturn
} = require('../../src/services/memo-multipush') } = require('../../src/services/memo-multipush')
const { encodeScript } = require('../support/script-encoding')
// Encode a script the way Bitcoin does: an opcode number is one byte, a
// Buffer/string becomes a length-prefixed push. Enough to observe the pushes.
function encodeScript (script) {
const parts = script.map((el) => {
if (typeof el === 'number') return Buffer.from([el])
const buf = Buffer.from(el)
return Buffer.concat([Buffer.from([buf.length]), buf])
})
return Buffer.concat(parts)
}
// A double of the minimal-slp-wallet surface the adapter reuses. // A double of the minimal-slp-wallet surface the adapter reuses.
function makeSlpWalletDouble () { function makeSlpWalletDouble () {
@@ -75,6 +65,24 @@ test('toPushBuffer encodes a string as UTF-8 and passes bytes through', () => {
assert.equal(toPushBuffer(Uint8Array.from([1, 2])).toString('hex'), '0102') assert.equal(toPushBuffer(Uint8Array.from([1, 2])).toString('hex'), '0102')
}) })
// The browser bundle has no global Buffer (CRA 5 does not polyfill Node
// globals and the wallet script does not set window.Buffer), so the adapter
// must import its own. Re-require it with the global removed to prove it.
test('the adapter builds pushes without a browser-global Buffer', () => {
const modulePath = require.resolve('../../src/services/memo-multipush')
const savedBuffer = global.Buffer
global.Buffer = undefined
delete require.cache[modulePath]
try {
const fresh = require('../../src/services/memo-multipush')
assert.equal(fresh.toPushBuffer('hi').toString('hex'), '6869')
assert.equal(fresh.buildPushes('6d03', ['hi'])[0].toString('hex'), '6d03')
} finally {
global.Buffer = savedBuffer
delete require.cache[modulePath]
}
})
test('buildPushes puts the prefix first and each field in order', () => { test('buildPushes puts the prefix first and each field in order', () => {
const pushes = buildPushes('6d03', [Buffer.from('aa', 'hex'), 'hi']) const pushes = buildPushes('6d03', [Buffer.from('aa', 'hex'), 'hi'])
@@ -116,13 +124,22 @@ test('a single field delegates to the original wallet sendOpReturn', async () =>
test('attaching twice does not double-wrap the wallet', async () => { test('attaching twice does not double-wrap the wallet', async () => {
const { wallet } = makeSlpWalletDouble() const { wallet } = makeSlpWalletDouble()
attachMultiPushOpReturn(wallet) attachMultiPushOpReturn(wallet)
const wrapped = wallet.sendOpReturn
attachMultiPushOpReturn(wallet) attachMultiPushOpReturn(wallet)
// The second attach must leave the wrapper untouched rather than nesting it.
assert.equal(wallet.sendOpReturn, wrapped)
await wallet.sendOpReturn('single', '6d02') await wallet.sendOpReturn('single', '6d02')
assert.equal(wallet.calls.length, 1) assert.equal(wallet.calls.length, 1)
}) })
test('attaching to a falsy wallet is a no-op', () => {
assert.equal(attachMultiPushOpReturn(null), null)
assert.equal(attachMultiPushOpReturn(undefined), undefined)
})
test('broadcastMultiPush rejects a wallet without the OP_RETURN builder', async () => { test('broadcastMultiPush rejects a wallet without the OP_RETURN builder', async () => {
await assert.rejects( await assert.rejects(
broadcastMultiPush({ opReturn: {} }, ['hi'], '6d02'), broadcastMultiPush({ opReturn: {} }, ['hi'], '6d02'),