From a1dd90700863f44fdc76beb8214e4fc28eba85a4 Mon Sep 17 00:00:00 2001 From: Paul Grey Date: Sun, 15 Mar 2026 19:29:57 +1300 Subject: [PATCH] Fix msig:propose serialization bug for actions with non-name fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The propose command failed with "Name should be less than 13 characters" when proposing msig transactions containing actions with u64/u128 fields (e.g. poolId: 265). The bug occurred because api.transact() recursively re-serialized already-serialized action data within the nested transaction, treating hex strings as structured JSON. Fix: manually serialize the inner transaction and propose action data to raw bytes, bypassing the library's recursive serialization. The inner actions are still properly serialized via api.serializeActions() using contract ABIs — only the final assembly is done manually. Co-Authored-By: Claude Opus 4.6 --- src/commands/msig/propose.ts | 78 ++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/src/commands/msig/propose.ts b/src/commands/msig/propose.ts index 9f75eba..c6e9320 100644 --- a/src/commands/msig/propose.ts +++ b/src/commands/msig/propose.ts @@ -5,6 +5,7 @@ import { CliUx } from '@oclif/core' import { green, red } from 'colors' import { getExplorer } from '../../apis/getExplorer' import { Authorization } from '@proton/wrap-constants' +import { Serialize } from '@proton/js' export default class MultisigPropose extends Command { static description = 'Multisig Propose' @@ -23,11 +24,11 @@ export default class MultisigPropose extends Command { async run() { const {args: {proposalName, actions, auth}, flags} = this.parse(MultisigPropose) const [actor, permission] = auth.split('@') - - // Serialize action + + // Serialize inner actions using the contract ABIs const parsedActions = JSON.parse(actions) const serializedActions = await network.api.serializeActions(parsedActions) - const transactionSettings = await network.protonApi.generateTransactionSettings(flags.expireSeconds, flags.blocksBehind, 0) + const transactionSettings = await network.protonApi.generateTransactionSettings(flags.expireSeconds, flags.blocksBehind, 0) as any // Find required signers let requested: Authorization[] = [] @@ -38,21 +39,72 @@ export default class MultisigPropose extends Command { } } requested = requested.filter((item, pos) => requested.findIndex(_ => _.actor === item.actor) === pos) - + + // Manually serialize the inner transaction to avoid the library's + // recursive re-serialization bug (it tries to re-parse already-serialized + // action data as structured JSON, causing "Name should be less than 13 + // characters" errors for actions with u64/u128 fields). + const trxBuf = new Serialize.SerialBuffer() + + // Transaction header + const expDate = new Date(transactionSettings.expiration + 'Z') + trxBuf.pushUint32(Math.floor(expDate.getTime() / 1000)) + trxBuf.pushUint16(transactionSettings.ref_block_num & 0xffff) + trxBuf.pushUint32(transactionSettings.ref_block_prefix) + trxBuf.pushVaruint32(0) // max_net_usage_words + trxBuf.push(0) // max_cpu_usage_ms + trxBuf.pushVaruint32(0) // delay_sec + + // context_free_actions (empty) + trxBuf.pushVaruint32(0) + + // actions + trxBuf.pushVaruint32(serializedActions.length) + for (const action of serializedActions) { + trxBuf.pushName(action.account) + trxBuf.pushName(action.name) + + // authorization + trxBuf.pushVaruint32(action.authorization.length) + for (const auth of action.authorization) { + trxBuf.pushName(auth.actor) + trxBuf.pushName(auth.permission) + } + + // data (already serialized hex) + const dataBytes = Buffer.from(action.data, 'hex') + trxBuf.pushVaruint32(dataBytes.length) + trxBuf.pushArray(dataBytes) + } + + // transaction_extensions (empty) + trxBuf.pushVaruint32(0) + + // Serialize the propose action data manually + const proposeBuf = new Serialize.SerialBuffer() + proposeBuf.pushName(actor) // proposer + proposeBuf.pushName(proposalName) // proposal_name + + // requested (permission_level[]) + proposeBuf.pushVaruint32(requested.length) + for (const req of requested) { + proposeBuf.pushName(req.actor) + proposeBuf.pushName(req.permission) + } + + // trx (inline transaction struct, not length-prefixed) + const trxBytes = trxBuf.asUint8Array() + proposeBuf.pushArray(trxBytes) + + const proposeDataHex = Buffer.from(proposeBuf.asUint8Array()).toString('hex') + try { + // Pass pre-serialized hex data to avoid recursive serialization await network.transact({ actions: [{ account: 'eosio.msig', name: 'propose', - data: { - proposer: actor, - proposal_name: proposalName, - requested, - trx: { - ...transactionSettings, - actions: serializedActions, - } - }, + data: proposeDataHex, authorization: [{ actor, permission: permission || 'active' }] }] })