mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Implement multi-push encoding for Memo multi-field actions
Reply, topic message, add-poll-option, poll-vote, and create-poll now broadcast each protocol field as its own OP_RETURN script push instead of combining them into one push. The indexer requires three pushes for reply, topic message, and poll children, and accepts four for create-poll; the combined form was rejected or misparsed. hex.js builds the txid and text as separate pushes, and a new memo-multipush adapter teaches minimal-slp-wallet's single-push sendOpReturn to expand an array of fields into separate pushes. Acceptance step handlers assert the individual pushes, and unit/property tests cover the new shape. By coder.
This commit is contained in:
@@ -58,6 +58,7 @@ const { renderPostOptions } = require('./render-post-options')
|
|||||||
const { renderLikeResult } = require('./render-like-result')
|
const { renderLikeResult } = require('./render-like-result')
|
||||||
const PostOptions = require('../../src/services/post-options')
|
const PostOptions = require('../../src/services/post-options')
|
||||||
const { YOUTUBE_EMBED_BASE_URL } = require('../../src/services/youtube-embed')
|
const { YOUTUBE_EMBED_BASE_URL } = require('../../src/services/youtube-embed')
|
||||||
|
const { toPushBuffer } = require('../../src/services/memo-multipush')
|
||||||
|
|
||||||
const MEMO_POST_PREFIX = MemoPost.MEMO_POST_PREFIX
|
const MEMO_POST_PREFIX = MemoPost.MEMO_POST_PREFIX
|
||||||
const MEMO_REPLY_PREFIX = MemoReply.MEMO_REPLY_PREFIX
|
const MEMO_REPLY_PREFIX = MemoReply.MEMO_REPLY_PREFIX
|
||||||
@@ -102,12 +103,19 @@ function makeWallet (address) {
|
|||||||
return this.utxos
|
return this.utxos
|
||||||
},
|
},
|
||||||
sendOpReturn: async function (msg, prefix, bchOutput = []) {
|
sendOpReturn: async function (msg, prefix, bchOutput = []) {
|
||||||
// Normalize binary payloads to Buffer so assertions can safely use
|
// Multi-field actions pass an array of field pushes. Expose each push
|
||||||
// toString('hex'), while preserving string payloads unchanged.
|
// separately for the multi-push assertions, while keeping `msg` as the
|
||||||
const storedMsg = (msg instanceof Uint8Array || ArrayBuffer.isView(msg))
|
// concatenated payload so older combined-payload assertions still work.
|
||||||
|
const isMulti = Array.isArray(msg)
|
||||||
|
const fields = isMulti ? msg : [msg]
|
||||||
|
const fieldBuffers = fields.map((field) => toPushBuffer(field))
|
||||||
|
const storedMsg = isMulti
|
||||||
|
? Buffer.concat(fieldBuffers)
|
||||||
|
: ((msg instanceof Uint8Array || ArrayBuffer.isView(msg))
|
||||||
? Buffer.from(msg)
|
? Buffer.from(msg)
|
||||||
: msg
|
: msg)
|
||||||
this.broadcasts.push({ msg: storedMsg, prefix, bchOutput })
|
const pushes = [Buffer.from(prefix, 'hex'), ...fieldBuffers]
|
||||||
|
this.broadcasts.push({ msg: storedMsg, pushes, prefix, bchOutput })
|
||||||
if (this.failWith) throw new Error(this.failWith)
|
if (this.failWith) throw new Error(this.failWith)
|
||||||
return 'aa'.repeat(32)
|
return 'aa'.repeat(32)
|
||||||
}
|
}
|
||||||
@@ -559,6 +567,28 @@ function decodeLikeTxid (raw) {
|
|||||||
return Buffer.from(raw).reverse().toString('hex')
|
return Buffer.from(raw).reverse().toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the most recent wallet broadcast, or fail when none was sent.
|
||||||
|
function lastBroadcast (world) {
|
||||||
|
const broadcasts = world.wallet.broadcasts
|
||||||
|
if (!broadcasts.length) throw new Error('No OP_RETURN transaction was broadcast.')
|
||||||
|
return broadcasts[broadcasts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the push at the given zero-based index from the most recent
|
||||||
|
// broadcast. Index 0 is the action prefix; later indexes are the fields.
|
||||||
|
function broadcastPush (world, index) {
|
||||||
|
const last = lastBroadcast(world)
|
||||||
|
if (!Array.isArray(last.pushes) || last.pushes.length <= index) {
|
||||||
|
throw new Error(`Broadcast does not have a push at index ${index}.`)
|
||||||
|
}
|
||||||
|
return Buffer.from(last.pushes[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse a display txid into little-endian wire hex.
|
||||||
|
function txidWireHex (txid) {
|
||||||
|
return Buffer.from(txid, 'hex').reverse().toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
// 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())
|
||||||
@@ -1046,6 +1076,94 @@ const handlers = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'wallet broadcasts multi-push OP_RETURN with prefix',
|
||||||
|
pattern: /^the wallet broadcasts an OP_RETURN with the Memo (reply|topic-message|add-poll-option|poll-vote|create-poll) prefix and (\d+) pushes$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const prefix = {
|
||||||
|
reply: MEMO_REPLY_PREFIX,
|
||||||
|
'topic-message': MEMO_TOPIC_MESSAGE_PREFIX,
|
||||||
|
'add-poll-option': MEMO_ADD_POLL_OPTION_PREFIX,
|
||||||
|
'poll-vote': MEMO_POLL_VOTE_PREFIX,
|
||||||
|
'create-poll': MEMO_CREATE_POLL_PREFIX
|
||||||
|
}[m[1]]
|
||||||
|
const expectedPushes = parseInt(m[2], 10)
|
||||||
|
const last = lastBroadcast(world)
|
||||||
|
if (last.prefix !== prefix) {
|
||||||
|
throw new Error(`Expected Memo ${m[1]} prefix ${prefix}, got "${last.prefix}".`)
|
||||||
|
}
|
||||||
|
const actual = Array.isArray(last.pushes) ? last.pushes.length : 0
|
||||||
|
if (actual !== expectedPushes) {
|
||||||
|
throw new Error(`Expected ${expectedPushes} OP_RETURN pushes, got ${actual}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second broadcast push is referenced txid in wire order',
|
||||||
|
pattern: /^the second broadcast push is the referenced txid (.+) in little-endian wire order$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const txid = resolveParam(m[1], example)
|
||||||
|
const push = broadcastPush(world, 1)
|
||||||
|
if (push.toString('hex') !== txidWireHex(txid)) {
|
||||||
|
throw new Error(`Second push did not match the wire txid ${txid}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second broadcast push is UTF-8 topic',
|
||||||
|
pattern: /^the second broadcast push is the UTF-8 topic "(.+)"$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const topic = resolveText(m[1], example)
|
||||||
|
const push = broadcastPush(world, 1)
|
||||||
|
if (push.toString('utf8') !== topic) {
|
||||||
|
throw new Error(`Second push "${push.toString('utf8')}" did not match topic "${topic}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second broadcast push is poll type',
|
||||||
|
pattern: /^the second broadcast push is the poll type (\d+)$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const expected = parseInt(m[1], 10)
|
||||||
|
const push = broadcastPush(world, 1)
|
||||||
|
if (push.length !== 1 || push[0] !== expected) {
|
||||||
|
throw new Error(`Second push was not the poll type ${expected}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'third broadcast push is UTF-8 text',
|
||||||
|
pattern: /^the third broadcast push is the UTF-8 text "(.+)"$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const text = resolveText(m[1], example)
|
||||||
|
const push = broadcastPush(world, 2)
|
||||||
|
if (push.toString('utf8') !== text) {
|
||||||
|
throw new Error(`Third push "${push.toString('utf8')}" did not match text "${text}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'third broadcast push is option count',
|
||||||
|
pattern: /^the third broadcast push is the option count (.+)$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const expected = parseInt(resolveParam(m[1], example), 10)
|
||||||
|
const push = broadcastPush(world, 2)
|
||||||
|
if (push.length !== 1 || push[0] !== expected) {
|
||||||
|
throw new Error(`Third push was not the option count ${expected}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fourth broadcast push is UTF-8 question',
|
||||||
|
pattern: /^the fourth broadcast push is the UTF-8 question "(.+)"$/,
|
||||||
|
run (m, example, world) {
|
||||||
|
const question = resolveText(m[1], example)
|
||||||
|
const push = broadcastPush(world, 3)
|
||||||
|
if (push.toString('utf8') !== question) {
|
||||||
|
throw new Error(`Fourth push "${push.toString('utf8')}" did not match question "${question}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'thread shows new reply from my address',
|
name: 'thread shows new reply from my address',
|
||||||
pattern: /^the thread shows a new reply from my address with the text "<([A-Za-z0-9_]+)>"$/,
|
pattern: /^the thread shows a new reply from my address with the text "<([A-Za-z0-9_]+)>"$/,
|
||||||
@@ -2223,7 +2341,8 @@ const handlers = [
|
|||||||
throw new Error(`Expected Memo topic-message prefix ${MEMO_TOPIC_MESSAGE_PREFIX}, got "${last.prefix}".`)
|
throw new Error(`Expected Memo topic-message prefix ${MEMO_TOPIC_MESSAGE_PREFIX}, got "${last.prefix}".`)
|
||||||
}
|
}
|
||||||
const expectedPayload = room + world.topicPostPage.input
|
const expectedPayload = room + world.topicPostPage.input
|
||||||
if (last.msg !== expectedPayload) {
|
const actualPayload = Buffer.isBuffer(last.msg) ? last.msg.toString('utf8') : last.msg
|
||||||
|
if (actualPayload !== expectedPayload) {
|
||||||
throw new Error(`Broadcast topic-message payload did not match ${room} + input.`)
|
throw new Error(`Broadcast topic-message payload did not match ${room} + input.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import GistServers from './gist-servers'
|
import GistServers from './gist-servers'
|
||||||
|
import memoMultipush from './memo-multipush'
|
||||||
|
|
||||||
class AsyncLoad {
|
class AsyncLoad {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -66,6 +67,10 @@ class AsyncLoad {
|
|||||||
|
|
||||||
this.wallet = wallet
|
this.wallet = wallet
|
||||||
|
|
||||||
|
// Teach the wallet to broadcast multi-field Memo actions as separate
|
||||||
|
// OP_RETURN pushes.
|
||||||
|
memoMultipush.attachMultiPushOpReturn(wallet)
|
||||||
|
|
||||||
return wallet
|
return wallet
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing wallet: ', error)
|
console.error('Error initializing wallet: ', error)
|
||||||
@@ -240,6 +245,10 @@ class AsyncLoad {
|
|||||||
|
|
||||||
this.wallet = wallet
|
this.wallet = wallet
|
||||||
|
|
||||||
|
// Teach the wallet to broadcast multi-field Memo actions as separate
|
||||||
|
// OP_RETURN pushes.
|
||||||
|
memoMultipush.attachMultiPushOpReturn(wallet)
|
||||||
|
|
||||||
return wallet
|
return wallet
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initStarterWallet: ', error)
|
console.error('Error initStarterWallet: ', error)
|
||||||
|
|||||||
@@ -33,19 +33,16 @@ function txidToWireBytes (txid, label = 'Value') {
|
|||||||
return hexToBytes(txid, 32, label).reverse()
|
return hexToBytes(txid, 32, label).reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the raw OP_RETURN payload for a txid-referencing Memo action: the
|
// Build the separate OP_RETURN pushes for a txid-referencing Memo action:
|
||||||
// given 32-byte txid in little-endian wire order followed by a UTF-8 encoded
|
// the referenced txid in little-endian wire order and the UTF-8 encoded value,
|
||||||
// value. The label customizes the invalid-txid error message.
|
// each as its own push. The label customizes the invalid-txid error message.
|
||||||
function buildTxidTextPayload (txid, text, label = 'Poll txid') {
|
function buildTxidTextPushes (txid, text, label = 'Poll txid') {
|
||||||
const txidBytes = txidToWireBytes(txid, label)
|
const txidBytes = txidToWireBytes(txid, label)
|
||||||
const textBytes = new TextEncoder().encode(text)
|
const textBytes = new TextEncoder().encode(text)
|
||||||
const raw = new Uint8Array(txidBytes.length + textBytes.length)
|
return [txidBytes, textBytes]
|
||||||
raw.set(txidBytes, 0)
|
|
||||||
raw.set(textBytes, txidBytes.length)
|
|
||||||
return raw
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { hexToBytes, txidToWireBytes, buildTxidTextPayload }
|
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-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"}]}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
Multi-push OP_RETURN adapter for Memo actions that carry several fields.
|
||||||
|
|
||||||
|
Memo multi-field actions (reply, topic message, add-poll-option, poll-vote,
|
||||||
|
and create-poll) must encode each protocol field as its own OP_RETURN script
|
||||||
|
push: OP_RETURN <prefix> <field> <field> ... . minimal-slp-wallet's
|
||||||
|
sendOpReturn(msg, prefix, bchOutput) pushes its msg as a single push, so this
|
||||||
|
adapter wraps a wallet's sendOpReturn: an array first argument is broadcast as
|
||||||
|
separate pushes, while every existing single-field call is delegated to the
|
||||||
|
original wallet method unchanged.
|
||||||
|
|
||||||
|
The adapter reuses the wallet's own OP_RETURN transaction builder for fee
|
||||||
|
selection, change, signing, and broadcast. Only the OP_RETURN script it
|
||||||
|
composes is expanded. The pure push-normalization helpers are unit tested;
|
||||||
|
the wallet wiring is the small environmentally unsuitable boundary.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Normalize one push value (a UTF-8 string or byte array) to a Buffer.
|
||||||
|
function toPushBuffer (value) {
|
||||||
|
if (typeof value === 'string') return Buffer.from(value, 'utf8')
|
||||||
|
if (value instanceof Uint8Array) return Buffer.from(value)
|
||||||
|
return Buffer.from(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the ordered OP_RETURN pushes: the action prefix bytes first, then each
|
||||||
|
// field as its own push.
|
||||||
|
function buildPushes (prefix, fields) {
|
||||||
|
return [Buffer.from(prefix, 'hex'), ...fields.map(toPushBuffer)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast fields as separate OP_RETURN pushes using the wallet's own
|
||||||
|
// transaction builder. The wallet must expose minimal-slp-wallet's
|
||||||
|
// opReturn.createTransaction and opReturn.ar.sendTx.
|
||||||
|
async function broadcastMultiPush (wallet, fields, prefix, bchOutput = []) {
|
||||||
|
const opReturn = wallet.opReturn
|
||||||
|
if (!opReturn || typeof opReturn.createTransaction !== 'function') {
|
||||||
|
throw new Error('Wallet does not support multi-push OP_RETURN broadcasts.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await wallet.walletInfoPromise
|
||||||
|
|
||||||
|
const bchjs = opReturn.bchjs
|
||||||
|
const originalEncode2 = bchjs.Script.encode2
|
||||||
|
const pushes = buildPushes(prefix, fields)
|
||||||
|
|
||||||
|
// createTransaction composes [OP_RETURN, prefix, msg] and calls encode2
|
||||||
|
// synchronously before any await. Swap in the multi-push script for that one
|
||||||
|
// call, then restore immediately so concurrent wallet work is unaffected.
|
||||||
|
bchjs.Script.encode2 = function (script) {
|
||||||
|
bchjs.Script.encode2 = originalEncode2
|
||||||
|
return originalEncode2.call(this, [script[0], ...pushes])
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { hex } = await opReturn.createTransaction(
|
||||||
|
wallet.walletInfo,
|
||||||
|
wallet.utxos.utxoStore.bchUtxos,
|
||||||
|
'',
|
||||||
|
prefix,
|
||||||
|
bchOutput,
|
||||||
|
wallet.fee
|
||||||
|
)
|
||||||
|
return await opReturn.ar.sendTx(hex)
|
||||||
|
} finally {
|
||||||
|
bchjs.Script.encode2 = originalEncode2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap a wallet's sendOpReturn so an array first argument broadcasts each
|
||||||
|
// element as its own OP_RETURN push. Single-field calls delegate unchanged.
|
||||||
|
function attachMultiPushOpReturn (wallet) {
|
||||||
|
if (!wallet || wallet.__multiPushAttached) return wallet
|
||||||
|
const original = wallet.sendOpReturn.bind(wallet)
|
||||||
|
wallet.sendOpReturn = function (msgOrFields, prefix, bchOutput = []) {
|
||||||
|
if (Array.isArray(msgOrFields)) {
|
||||||
|
return broadcastMultiPush(wallet, msgOrFields, prefix, bchOutput)
|
||||||
|
}
|
||||||
|
return original(msgOrFields, prefix, bchOutput)
|
||||||
|
}
|
||||||
|
wallet.__multiPushAttached = true
|
||||||
|
return wallet
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
toPushBuffer,
|
||||||
|
buildPushes,
|
||||||
|
broadcastMultiPush,
|
||||||
|
attachMultiPushOpReturn
|
||||||
|
}
|
||||||
@@ -62,8 +62,8 @@ class MemoPollCreate extends MemoAction {
|
|||||||
|
|
||||||
await this.wallet.getUtxos()
|
await this.wallet.getUtxos()
|
||||||
|
|
||||||
const raw = buildCreatePollPayload(question, this.pollType, count)
|
const pushes = buildCreatePollPushes(question, this.pollType, count)
|
||||||
const txid = await this.wallet.sendOpReturn(raw, this.prefix)
|
const txid = await this.wallet.sendOpReturn(pushes, this.prefix)
|
||||||
|
|
||||||
this.reflect(txid, question, count)
|
this.reflect(txid, question, count)
|
||||||
|
|
||||||
@@ -84,15 +84,15 @@ class MemoPollCreate extends MemoAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the raw OP_RETURN message payload for a create-poll action.
|
// Build the separate OP_RETURN pushes for a create-poll action: the poll type
|
||||||
// The protocol wire format is: <poll_type 1 byte><option_count 1 byte><question UTF-8 bytes>.
|
// byte, the option count byte, and the UTF-8 question, each as its own push.
|
||||||
function buildCreatePollPayload (question, pollType, optionCount) {
|
function buildCreatePollPushes (question, pollType, optionCount) {
|
||||||
const textBytes = new TextEncoder().encode(question)
|
const textBytes = new TextEncoder().encode(question)
|
||||||
const raw = new Uint8Array(2 + textBytes.length)
|
return [
|
||||||
raw[0] = pollType & 0xff
|
Uint8Array.from([pollType & 0xff]),
|
||||||
raw[1] = optionCount & 0xff
|
Uint8Array.from([optionCount & 0xff]),
|
||||||
raw.set(textBytes, 2)
|
textBytes
|
||||||
return raw
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
MemoPollCreate.MEMO_CREATE_POLL_PREFIX = MEMO_CREATE_POLL_PREFIX
|
MemoPollCreate.MEMO_CREATE_POLL_PREFIX = MEMO_CREATE_POLL_PREFIX
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const MemoTxidAction = require('./memo-txid-action')
|
const MemoTxidAction = require('./memo-txid-action')
|
||||||
const { buildTxidTextPayload } = require('./hex')
|
const { buildTxidTextPushes } = require('./hex')
|
||||||
|
|
||||||
const MEMO_ADD_POLL_OPTION_PREFIX = '6d13'
|
const MEMO_ADD_POLL_OPTION_PREFIX = '6d13'
|
||||||
const MAX_OPTION_BYTES = 184
|
const MAX_OPTION_BYTES = 184
|
||||||
@@ -35,7 +35,7 @@ class MemoPollOption extends MemoTxidAction {
|
|||||||
|
|
||||||
// Compose and broadcast a Memo add-poll-option action.
|
// Compose and broadcast a Memo add-poll-option action.
|
||||||
add (option) {
|
add (option) {
|
||||||
return this.broadcastTxid(option, buildTxidTextPayload)
|
return this.broadcastTxid(option, buildTxidTextPushes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const MemoTxidAction = require('./memo-txid-action')
|
const MemoTxidAction = require('./memo-txid-action')
|
||||||
const { buildTxidTextPayload } = require('./hex')
|
const { buildTxidTextPushes } = require('./hex')
|
||||||
|
|
||||||
const MEMO_POLL_VOTE_PREFIX = '6d14'
|
const MEMO_POLL_VOTE_PREFIX = '6d14'
|
||||||
const MAX_COMMENT_BYTES = 184
|
const MAX_COMMENT_BYTES = 184
|
||||||
@@ -35,7 +35,7 @@ class MemoPollVote extends MemoTxidAction {
|
|||||||
|
|
||||||
// Compose and broadcast a Memo poll-vote action.
|
// Compose and broadcast a Memo poll-vote action.
|
||||||
vote (comment) {
|
vote (comment) {
|
||||||
return this.broadcastTxid(comment, buildTxidTextPayload)
|
return this.broadcastTxid(comment, buildTxidTextPushes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
const MemoAction = require('./memo-action')
|
const MemoAction = require('./memo-action')
|
||||||
const { byteLength } = require('./utf8')
|
const { byteLength } = require('./utf8')
|
||||||
const { buildTxidTextPayload } = require('./hex')
|
const { buildTxidTextPushes } = require('./hex')
|
||||||
|
|
||||||
const MEMO_REPLY_PREFIX = '6d03'
|
const MEMO_REPLY_PREFIX = '6d03'
|
||||||
const MAX_REPLY_BYTES = 184
|
const MAX_REPLY_BYTES = 184
|
||||||
@@ -56,9 +56,9 @@ class MemoReply extends MemoAction {
|
|||||||
// Refresh the wallet's spendable UTXO store so the broadcast has inputs.
|
// Refresh the wallet's spendable UTXO store so the broadcast has inputs.
|
||||||
await this.wallet.getUtxos()
|
await this.wallet.getUtxos()
|
||||||
|
|
||||||
// Build the raw payload: parent txid bytes followed by UTF-8 message bytes.
|
// Build the separate pushes: parent txid bytes, then UTF-8 message bytes.
|
||||||
const raw = buildTxidTextPayload(parentTxid, message, 'Parent txid')
|
const pushes = buildTxidTextPushes(parentTxid, message, 'Parent txid')
|
||||||
const txid = await this.wallet.sendOpReturn(raw, this.prefix)
|
const txid = await this.wallet.sendOpReturn(pushes, this.prefix)
|
||||||
|
|
||||||
// Reflect the result on the injected thread once broadcast succeeds.
|
// Reflect the result on the injected thread once broadcast succeeds.
|
||||||
this.reflect(txid, message, parentTxid)
|
this.reflect(txid, message, parentTxid)
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ class MemoTopicPost extends MemoAction {
|
|||||||
|
|
||||||
await this.wallet.getUtxos()
|
await this.wallet.getUtxos()
|
||||||
|
|
||||||
const payload = this.room + message
|
const pushes = [
|
||||||
const txid = await this.wallet.sendOpReturn(payload, this.prefix)
|
new TextEncoder().encode(this.room),
|
||||||
|
new TextEncoder().encode(message)
|
||||||
|
]
|
||||||
|
const txid = await this.wallet.sendOpReturn(pushes, this.prefix)
|
||||||
|
|
||||||
this.reflect(txid, message)
|
this.reflect(txid, message)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
These pin down invariants over broad random inputs that the unit tests only
|
These pin down invariants over broad random inputs that the unit tests only
|
||||||
probe at fixed fixtures:
|
probe at fixed fixtures:
|
||||||
|
|
||||||
- Round-trip: buildTxidTextPayload encodes a poll txid + text into the
|
- Round-trip: buildTxidTextPushes encodes a poll txid + text into the
|
||||||
canonical Memo wire payload, so the stored reverse-hex txid and the
|
canonical Memo wire pushes, so the stored reverse-hex txid and the
|
||||||
UTF-8 text both decode back unchanged.
|
UTF-8 text both decode back unchanged.
|
||||||
- hexToBytes length contract: only 64-character hex txids decode to
|
- hexToBytes length contract: only 64-character hex txids decode to
|
||||||
32 bytes; everything else throws.
|
32 bytes; everything else throws.
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
const test = require('node:test')
|
const test = require('node:test')
|
||||||
const { seededRandom, forAll } = require('./harness')
|
const { seededRandom, forAll } = require('./harness')
|
||||||
const { hexToBytes, buildTxidTextPayload } = require('../../src/services/hex')
|
const { hexToBytes, buildTxidTextPushes } = require('../../src/services/hex')
|
||||||
const { byteLength } = require('../../src/services/utf8')
|
const { byteLength } = require('../../src/services/utf8')
|
||||||
const MemoPollOption = require('../../src/services/memo-poll-option')
|
const MemoPollOption = require('../../src/services/memo-poll-option')
|
||||||
const MemoPollVote = require('../../src/services/memo-poll-vote')
|
const MemoPollVote = require('../../src/services/memo-poll-vote')
|
||||||
@@ -61,13 +61,14 @@ function makeWallet () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('buildTxidTextPayload round-trips the canonical Memo wire format', async () => {
|
test('buildTxidTextPushes round-trips the canonical Memo wire format', async () => {
|
||||||
await forAll(
|
await forAll(
|
||||||
() => ({ txid: randomTxid(), text: randomText(200) }),
|
() => ({ txid: randomTxid(), text: randomText(200) }),
|
||||||
({ txid, text }) => {
|
({ txid, text }) => {
|
||||||
const bytes = buildTxidTextPayload(txid, text)
|
const pushes = buildTxidTextPushes(txid, text)
|
||||||
|
const bytes = Buffer.concat(pushes.map((p) => Buffer.from(p)))
|
||||||
if (bytes.length !== 32 + byteLength(text)) return false
|
if (bytes.length !== 32 + byteLength(text)) return false
|
||||||
// The payload carries the txid in little-endian wire order, followed by
|
// The pushes carry the txid in little-endian wire order, followed by
|
||||||
// the UTF-8 bytes of the value. Reversing the wire bytes must recover
|
// the UTF-8 bytes of the value. Reversing the wire bytes must recover
|
||||||
// the 64-character display txid.
|
// the 64-character display txid.
|
||||||
const wire = Buffer.from(bytes.slice(0, 32))
|
const wire = Buffer.from(bytes.slice(0, 32))
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
const test = require('node:test')
|
const test = require('node:test')
|
||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
const { seededRandom, forAll, intGen } = require('./harness')
|
const { seededRandom, forAll, intGen } = require('./harness')
|
||||||
const { txidToWireBytes, buildTxidTextPayload } = require('../../src/services/hex')
|
const { txidToWireBytes, buildTxidTextPushes } = require('../../src/services/hex')
|
||||||
|
|
||||||
const HEX_CHARS = '0123456789abcdef'
|
const HEX_CHARS = '0123456789abcdef'
|
||||||
|
|
||||||
@@ -87,8 +87,8 @@ test('payload embeds the wire txid followed by the UTF-8 text', async () => {
|
|||||||
await forAll(
|
await forAll(
|
||||||
() => ({ txid: randomTxid(rng), text: randomText(rng) }),
|
() => ({ txid: randomTxid(rng), text: randomText(rng) }),
|
||||||
({ txid, text }) => {
|
({ txid, text }) => {
|
||||||
const raw = buildTxidTextPayload(txid, text)
|
const pushes = buildTxidTextPushes(txid, text)
|
||||||
const buf = Buffer.from(raw)
|
const buf = Buffer.concat(pushes.map((p) => Buffer.from(p)))
|
||||||
const wire = Buffer.from(txidToWireBytes(txid)).toString('hex')
|
const wire = Buffer.from(txidToWireBytes(txid)).toString('hex')
|
||||||
const expectedText = Buffer.from(text, 'utf8')
|
const expectedText = Buffer.from(text, 'utf8')
|
||||||
|
|
||||||
@@ -96,13 +96,13 @@ test('payload embeds the wire txid followed by the UTF-8 text', async () => {
|
|||||||
if (buf.subarray(0, 32).toString('hex') !== wire) return false
|
if (buf.subarray(0, 32).toString('hex') !== wire) return false
|
||||||
return buf.subarray(32).equals(expectedText)
|
return buf.subarray(32).equals(expectedText)
|
||||||
},
|
},
|
||||||
{ label: 'buildTxidTextPayload shape' }
|
{ label: 'buildTxidTextPushes shape' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('a reply payload rejects an invalid txid with the parent label', () => {
|
test('a reply payload rejects an invalid txid with the parent label', () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => buildTxidTextPayload('not-a-txid', 'hi', 'Parent txid'),
|
() => buildTxidTextPushes('not-a-txid', 'hi', 'Parent txid'),
|
||||||
/Parent txid must be a 64-character hex string/
|
/Parent txid must be a 64-character hex string/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
Memo actions that embed a parent poll txid use hexToBytes to decode the
|
Memo actions that embed a parent poll txid use hexToBytes to decode the
|
||||||
64-character hex txid into 32 raw bytes. These direct tests pin the length
|
64-character hex txid into 32 raw bytes. These direct tests pin the length
|
||||||
and hex-validity guards independently of the memo-poll broadcast path that
|
and hex-validity guards independently of the memo-poll broadcast path that
|
||||||
also reaches hexToBytes through buildTxidTextPayload.
|
also reaches hexToBytes through buildTxidTextPushes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const test = require('node:test')
|
const test = require('node:test')
|
||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
const { hexToBytes, buildTxidTextPayload, txidToWireBytes } = require('../../src/services/hex')
|
const { hexToBytes, buildTxidTextPushes, txidToWireBytes } = require('../../src/services/hex')
|
||||||
|
|
||||||
// A non-palindromic txid so a missing byte reversal is observable.
|
// A non-palindromic txid so a missing byte reversal is observable.
|
||||||
const DISPLAY_TXID = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
const DISPLAY_TXID = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||||
@@ -57,13 +57,13 @@ test('hexToBytes rejects a non-string value', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('buildTxidTextPayload prefixes the raw txid bytes', () => {
|
test('buildTxidTextPushes returns the txid and text as separate pushes', () => {
|
||||||
const raw = buildTxidTextPayload('ab'.repeat(32), 'hi')
|
const pushes = buildTxidTextPushes('ab'.repeat(32), 'hi')
|
||||||
const buf = Buffer.from(raw)
|
|
||||||
|
|
||||||
assert.equal(buf.length, 32 + 2)
|
assert.equal(pushes.length, 2)
|
||||||
assert.equal(buf[0], 0xab)
|
assert.equal(Buffer.from(pushes[0]).length, 32)
|
||||||
assert.equal(buf.slice(32).toString('utf8'), 'hi')
|
assert.equal(Buffer.from(pushes[0])[0], 0xab)
|
||||||
|
assert.equal(Buffer.from(pushes[1]).toString('utf8'), 'hi')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('txidToWireBytes reverses the display txid into little-endian wire order', () => {
|
test('txidToWireBytes reverses the display txid into little-endian wire order', () => {
|
||||||
@@ -80,10 +80,9 @@ test('txidToWireBytes rejects an invalid txid', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('buildTxidTextPayload embeds the txid in little-endian wire order', () => {
|
test('buildTxidTextPushes embeds the txid in little-endian wire order', () => {
|
||||||
const raw = buildTxidTextPayload(DISPLAY_TXID, 'hi')
|
const pushes = buildTxidTextPushes(DISPLAY_TXID, 'hi')
|
||||||
const buf = Buffer.from(raw)
|
|
||||||
|
|
||||||
assert.equal(buf.slice(0, 32).toString('hex'), WIRE_HEX)
|
assert.equal(Buffer.from(pushes[0]).toString('hex'), WIRE_HEX)
|
||||||
assert.equal(buf.slice(32).toString('utf8'), 'hi')
|
assert.equal(Buffer.from(pushes[1]).toString('utf8'), 'hi')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for the multi-push OP_RETURN adapter.
|
||||||
|
|
||||||
|
Memo actions that carry more than one field must encode each field as its
|
||||||
|
own OP_RETURN script push. minimal-slp-wallet broadcasts a single msg push,
|
||||||
|
so the adapter wraps the wallet's sendOpReturn: an array first argument is
|
||||||
|
expanded to separate pushes, while single-field calls keep delegating to the
|
||||||
|
original wallet method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const test = require('node:test')
|
||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const {
|
||||||
|
toPushBuffer,
|
||||||
|
buildPushes,
|
||||||
|
attachMultiPushOpReturn
|
||||||
|
} = require('../../src/services/memo-multipush')
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
function makeSlpWalletDouble () {
|
||||||
|
const sent = []
|
||||||
|
const wallet = {
|
||||||
|
walletInfo: { cashAddress: 'bitcoincash:qtest' },
|
||||||
|
fee: 1,
|
||||||
|
walletInfoPromise: Promise.resolve(),
|
||||||
|
utxos: { utxoStore: { bchUtxos: [{ tx_hash: 'aa', tx_pos: 0, value: 100000 }] } },
|
||||||
|
calls: [],
|
||||||
|
async sendOpReturn (msg, prefix, bchOutput = []) {
|
||||||
|
this.calls.push({ msg, prefix, bchOutput })
|
||||||
|
return 'single-txid'
|
||||||
|
},
|
||||||
|
opReturn: {
|
||||||
|
bchjs: {
|
||||||
|
Script: {
|
||||||
|
opcodes: { OP_RETURN: 0x6a },
|
||||||
|
encode2: encodeScript
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createTransaction (walletInfo, bchUtxos, msg, prefix) {
|
||||||
|
this.lastScript = [
|
||||||
|
this.bchjs.Script.opcodes.OP_RETURN,
|
||||||
|
Buffer.from(prefix, 'hex'),
|
||||||
|
Buffer.from(msg)
|
||||||
|
]
|
||||||
|
this.lastEncoded = this.bchjs.Script.encode2(this.lastScript)
|
||||||
|
return { hex: 'beef' }
|
||||||
|
},
|
||||||
|
ar: {
|
||||||
|
async sendTx (hex) {
|
||||||
|
sent.push(hex)
|
||||||
|
return 'txid-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { wallet, sent }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('toPushBuffer encodes a string as UTF-8 and passes bytes through', () => {
|
||||||
|
assert.equal(toPushBuffer('hi').toString('hex'), '6869')
|
||||||
|
assert.equal(toPushBuffer(Uint8Array.from([1, 2])).toString('hex'), '0102')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildPushes puts the prefix first and each field in order', () => {
|
||||||
|
const pushes = buildPushes('6d03', [Buffer.from('aa', 'hex'), 'hi'])
|
||||||
|
|
||||||
|
assert.equal(pushes.length, 3)
|
||||||
|
assert.equal(pushes[0].toString('hex'), '6d03')
|
||||||
|
assert.equal(pushes[1].toString('hex'), 'aa')
|
||||||
|
assert.equal(pushes[2].toString('utf8'), 'hi')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('array fields broadcast as separate OP_RETURN pushes', async () => {
|
||||||
|
const { wallet, sent } = makeSlpWalletDouble()
|
||||||
|
attachMultiPushOpReturn(wallet)
|
||||||
|
|
||||||
|
const txid = await wallet.sendOpReturn(
|
||||||
|
[Buffer.from('aabb', 'hex'), Buffer.from('hello')],
|
||||||
|
'6d03'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(txid, 'txid-1')
|
||||||
|
assert.deepEqual(sent, ['beef'])
|
||||||
|
// OP_RETURN, push(6d03), push(aabb), push(hello) — each its own push.
|
||||||
|
assert.equal(
|
||||||
|
wallet.opReturn.lastEncoded.toString('hex'),
|
||||||
|
'6a026d0302aabb0568656c6c6f'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a single field delegates to the original wallet sendOpReturn', async () => {
|
||||||
|
const { wallet } = makeSlpWalletDouble()
|
||||||
|
attachMultiPushOpReturn(wallet)
|
||||||
|
|
||||||
|
const txid = await wallet.sendOpReturn('single', '6d02', [])
|
||||||
|
|
||||||
|
assert.equal(txid, 'single-txid')
|
||||||
|
assert.equal(wallet.calls.length, 1)
|
||||||
|
assert.equal(wallet.calls[0].msg, 'single')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('attaching twice does not double-wrap the wallet', async () => {
|
||||||
|
const { wallet } = makeSlpWalletDouble()
|
||||||
|
attachMultiPushOpReturn(wallet)
|
||||||
|
attachMultiPushOpReturn(wallet)
|
||||||
|
|
||||||
|
await wallet.sendOpReturn('single', '6d02')
|
||||||
|
|
||||||
|
assert.equal(wallet.calls.length, 1)
|
||||||
|
})
|
||||||
@@ -28,16 +28,7 @@ function makeWallet (address = MY_ADDRESS) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodePayload (raw) {
|
test('create broadcasts the poll type, option count, and question as separate pushes', async () => {
|
||||||
const buf = Buffer.from(raw)
|
|
||||||
return {
|
|
||||||
pollType: buf[0],
|
|
||||||
optionCount: buf[1],
|
|
||||||
question: buf.slice(2).toString('utf8')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('create broadcasts with the create-poll prefix and payload', async () => {
|
|
||||||
const wallet = makeWallet()
|
const wallet = makeWallet()
|
||||||
const memoPollCreate = new MemoPollCreate({ wallet })
|
const memoPollCreate = new MemoPollCreate({ wallet })
|
||||||
|
|
||||||
@@ -45,10 +36,12 @@ test('create broadcasts with the create-poll prefix and payload', async () => {
|
|||||||
|
|
||||||
assert.equal(wallet.broadcasts.length, 1)
|
assert.equal(wallet.broadcasts.length, 1)
|
||||||
assert.equal(wallet.broadcasts[0].prefix, MemoPollCreate.MEMO_CREATE_POLL_PREFIX)
|
assert.equal(wallet.broadcasts[0].prefix, MemoPollCreate.MEMO_CREATE_POLL_PREFIX)
|
||||||
const decoded = decodePayload(wallet.broadcasts[0].msg)
|
const pushes = wallet.broadcasts[0].msg
|
||||||
assert.equal(decoded.question, 'which is better?')
|
assert.ok(Array.isArray(pushes), 'expected separate pushes')
|
||||||
assert.equal(decoded.optionCount, 2)
|
assert.equal(pushes.length, 3)
|
||||||
assert.equal(decoded.pollType, 1)
|
assert.equal(Buffer.from(pushes[0])[0], 1)
|
||||||
|
assert.equal(Buffer.from(pushes[1])[0], 2)
|
||||||
|
assert.equal(Buffer.from(pushes[2]).toString('utf8'), 'which is better?')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('create accepts an option count of one', async () => {
|
test('create accepts an option count of one', async () => {
|
||||||
|
|||||||
@@ -30,14 +30,7 @@ function makeWallet (address = MY_ADDRESS) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodePayload (raw) {
|
test('add broadcasts the poll txid and option as separate pushes', async () => {
|
||||||
const buf = Buffer.from(raw)
|
|
||||||
const pollTxid = Buffer.from(buf.slice(0, 32)).reverse().toString('hex')
|
|
||||||
const option = buf.slice(32).toString('utf8')
|
|
||||||
return { pollTxid, option }
|
|
||||||
}
|
|
||||||
|
|
||||||
test('add broadcasts with the add-poll-option prefix and payload', async () => {
|
|
||||||
const wallet = makeWallet()
|
const wallet = makeWallet()
|
||||||
const memoPollOption = new MemoPollOption({ wallet, pollTxid: POLL_TXID })
|
const memoPollOption = new MemoPollOption({ wallet, pollTxid: POLL_TXID })
|
||||||
|
|
||||||
@@ -45,9 +38,11 @@ test('add broadcasts with the add-poll-option prefix and payload', async () => {
|
|||||||
|
|
||||||
assert.equal(wallet.broadcasts.length, 1)
|
assert.equal(wallet.broadcasts.length, 1)
|
||||||
assert.equal(wallet.broadcasts[0].prefix, MemoPollOption.MEMO_ADD_POLL_OPTION_PREFIX)
|
assert.equal(wallet.broadcasts[0].prefix, MemoPollOption.MEMO_ADD_POLL_OPTION_PREFIX)
|
||||||
const decoded = decodePayload(wallet.broadcasts[0].msg)
|
const pushes = wallet.broadcasts[0].msg
|
||||||
assert.equal(decoded.pollTxid, POLL_TXID)
|
assert.ok(Array.isArray(pushes), 'expected separate pushes')
|
||||||
assert.equal(decoded.option, 'yes')
|
assert.equal(pushes.length, 2)
|
||||||
|
assert.equal(Buffer.from(pushes[0]).reverse().toString('hex'), POLL_TXID)
|
||||||
|
assert.equal(Buffer.from(pushes[1]).toString('utf8'), 'yes')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('add reflects the new option on the injected poll store', async () => {
|
test('add reflects the new option on the injected poll store', async () => {
|
||||||
|
|||||||
@@ -30,14 +30,7 @@ function makeWallet (address = MY_ADDRESS) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodePayload (raw) {
|
test('vote broadcasts the poll txid and comment as separate pushes', async () => {
|
||||||
const buf = Buffer.from(raw)
|
|
||||||
const pollTxid = Buffer.from(buf.slice(0, 32)).reverse().toString('hex')
|
|
||||||
const comment = buf.slice(32).toString('utf8')
|
|
||||||
return { pollTxid, comment }
|
|
||||||
}
|
|
||||||
|
|
||||||
test('vote broadcasts with the poll-vote prefix and payload', async () => {
|
|
||||||
const wallet = makeWallet()
|
const wallet = makeWallet()
|
||||||
const memoPollVote = new MemoPollVote({ wallet, pollTxid: POLL_TXID })
|
const memoPollVote = new MemoPollVote({ wallet, pollTxid: POLL_TXID })
|
||||||
|
|
||||||
@@ -45,9 +38,11 @@ test('vote broadcasts with the poll-vote prefix and payload', async () => {
|
|||||||
|
|
||||||
assert.equal(wallet.broadcasts.length, 1)
|
assert.equal(wallet.broadcasts.length, 1)
|
||||||
assert.equal(wallet.broadcasts[0].prefix, MemoPollVote.MEMO_POLL_VOTE_PREFIX)
|
assert.equal(wallet.broadcasts[0].prefix, MemoPollVote.MEMO_POLL_VOTE_PREFIX)
|
||||||
const decoded = decodePayload(wallet.broadcasts[0].msg)
|
const pushes = wallet.broadcasts[0].msg
|
||||||
assert.equal(decoded.pollTxid, POLL_TXID)
|
assert.ok(Array.isArray(pushes), 'expected separate pushes')
|
||||||
assert.equal(decoded.comment, 'yes')
|
assert.equal(pushes.length, 2)
|
||||||
|
assert.equal(Buffer.from(pushes[0]).reverse().toString('hex'), POLL_TXID)
|
||||||
|
assert.equal(Buffer.from(pushes[1]).toString('utf8'), 'yes')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('vote reflects the new vote on the injected poll store', async () => {
|
test('vote reflects the new vote on the injected poll store', async () => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function makeWallet () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('reply broadcasts the parent txid in little-endian wire order', async () => {
|
test('reply broadcasts the parent txid and text as separate pushes', async () => {
|
||||||
const wallet = makeWallet()
|
const wallet = makeWallet()
|
||||||
const memoReply = new MemoReply({ wallet })
|
const memoReply = new MemoReply({ wallet })
|
||||||
|
|
||||||
@@ -39,9 +39,11 @@ test('reply broadcasts the parent txid in little-endian wire order', async () =>
|
|||||||
|
|
||||||
assert.equal(wallet.broadcasts.length, 1)
|
assert.equal(wallet.broadcasts.length, 1)
|
||||||
assert.equal(wallet.broadcasts[0].prefix, MemoReply.MEMO_REPLY_PREFIX)
|
assert.equal(wallet.broadcasts[0].prefix, MemoReply.MEMO_REPLY_PREFIX)
|
||||||
const buf = Buffer.from(wallet.broadcasts[0].msg)
|
const pushes = wallet.broadcasts[0].msg
|
||||||
assert.equal(buf.slice(0, 32).toString('hex'), WIRE_HEX)
|
assert.ok(Array.isArray(pushes), 'expected separate pushes')
|
||||||
assert.equal(buf.slice(32).toString('utf8'), 'hello memo')
|
assert.equal(pushes.length, 2)
|
||||||
|
assert.equal(Buffer.from(pushes[0]).toString('hex'), WIRE_HEX)
|
||||||
|
assert.equal(Buffer.from(pushes[1]).toString('utf8'), 'hello memo')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isTooLong accepts a message exactly at the byte limit', () => {
|
test('isTooLong accepts a message exactly at the byte limit', () => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function makeWallet (address = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('post broadcasts topic name and message with the topic-message prefix', async () => {
|
test('post broadcasts topic name and message as separate pushes', async () => {
|
||||||
const wallet = makeWallet()
|
const wallet = makeWallet()
|
||||||
const memoTopicPost = new MemoTopicPost({ wallet, room: 'bitcoin' })
|
const memoTopicPost = new MemoTopicPost({ wallet, room: 'bitcoin' })
|
||||||
|
|
||||||
@@ -34,7 +34,11 @@ test('post broadcasts topic name and message with the topic-message prefix', asy
|
|||||||
|
|
||||||
assert.equal(wallet.broadcasts.length, 1)
|
assert.equal(wallet.broadcasts.length, 1)
|
||||||
assert.equal(wallet.broadcasts[0].prefix, MemoTopicPost.MEMO_TOPIC_MESSAGE_PREFIX)
|
assert.equal(wallet.broadcasts[0].prefix, MemoTopicPost.MEMO_TOPIC_MESSAGE_PREFIX)
|
||||||
assert.equal(wallet.broadcasts[0].msg, 'bitcoinhello bitcoin')
|
const pushes = wallet.broadcasts[0].msg
|
||||||
|
assert.ok(Array.isArray(pushes), 'expected separate pushes')
|
||||||
|
assert.equal(pushes.length, 2)
|
||||||
|
assert.equal(Buffer.from(pushes[0]).toString('utf8'), 'bitcoin')
|
||||||
|
assert.equal(Buffer.from(pushes[1]).toString('utf8'), 'hello bitcoin')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('post reflects the new topic post on the injected feed', async () => {
|
test('post reflects the new topic post on the injected feed', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user