Skip to content

Relay serialization

The WizardConnect relay transmits messages as JSON, which cannot represent BigInt or Uint8Array natively. @wizardconnect/core provides canonical encoding helpers so dapps and wallets always agree on the wire format.

The helpers are split into two layers:

  • Generic (serialize.ts) — relay-level type coercion (toUint8Array, toBigInt, parseExtendedJson). Useful for any protocol.
  • hdwalletv1 (protocols/hdwalletv1-serialize.ts) — transaction-specific serialization (sourceOutputToRelay, transactionToHex). Tied to the sign-transaction flow.

Encoding conventions

Native type Relay format Example
Uint8Array hex string "76a914...88ac"
Uint8Array extended format (libauth stringify) "<Uint8Array: 0x76a914...>"
BigInt extended format "<bigint: 200000n>"

Both extended formats are accepted by the deserialization helpers. The serialization helpers produce hex strings for Uint8Array and <bigint: Xn> for BigInt.

hdwalletv1 serialization (dapp → relay)

sourceOutputToRelay(sourceOutput)

Converts a source output with native types to relay-safe JSON:

import { sourceOutputToRelay } from "@wizardconnect/core/hdwalletv1-serialize";

const relayOutput = sourceOutputToRelay({
  outpointTransactionHash: txidBytes,     // Uint8Array → hex string
  outpointIndex: 0,                       // number (unchanged)
  unlockingBytecode: new Uint8Array(0),   // Uint8Array → hex string
  sequenceNumber: 0xffffffff,             // number (unchanged)
  valueSatoshis: 200000n,                 // BigInt → "<bigint: 200000n>"
  lockingBytecode: scriptBytes,           // Uint8Array → hex string
  token: {                                // optional
    category: categoryBytes,              // Uint8Array → hex string
    amount: 1000n,                        // BigInt → "<bigint: 1000n>"
  },
});

// relayOutput is JSON-serializable (no BigInt, no Uint8Array)
JSON.stringify(relayOutput); // works

transactionToHex(inputs, outputs, version?, locktime?)

Encodes a transaction to hex using libauth's encodeTransaction:

import { transactionToHex } from "@wizardconnect/core/hdwalletv1-serialize";

const txHex = transactionToHex(inputs, outputs);
// txHex is a hex string ready for the relay

Note: libauth's encodeTransaction reverses outpointTransactionHash to wire format internally. Pass txids in display order (big-endian, as returned by electrum/explorers).

Deserialization (relay → wallet)

toUint8Array(value)

Converts hex strings, extended JSON format, or Uint8Array to Uint8Array:

import { toUint8Array } from "@wizardconnect/core";

toUint8Array("76a914...88ac");                  // hex string
toUint8Array("<Uint8Array: 0x76a914...88ac>");  // extended format
toUint8Array(existingBytes);                     // pass-through

toBigInt(value)

Converts numeric strings, extended JSON format, numbers, or bigint to bigint:

import { toBigInt } from "@wizardconnect/core";

toBigInt("<bigint: 200000n>");  // extended format
toBigInt("200000");             // numeric string
toBigInt(200000);               // number
toBigInt(200000n);              // pass-through

parseExtendedJson(jsonString)

Parses a full JSON string, converting all extended-format values in one pass:

import { parseExtendedJson } from "@wizardconnect/core";

const obj = parseExtendedJson('{"value":"<bigint: 200000n>","data":"<Uint8Array: 0xab>"}');
// obj.value === 200000n
// obj.data instanceof Uint8Array

isExtendedJsonFormat(str)

Returns true if a string contains <bigint: ...> or <Uint8Array: ...> markers.

Usage in sign requests

A typical dapp builds a sign request like this:

import { RelayMsgAction } from "@wizardconnect/core";
import { sourceOutputToRelay, transactionToHex } from "@wizardconnect/core/hdwalletv1-serialize";

const txHex = transactionToHex(inputs, outputs);
const sourceOutputs = inputs.map((input, i) =>
  sourceOutputToRelay({
    ...input,
    valueSatoshis: utxos[i].value,
    lockingBytecode: utxos[i].script,
  })
);

const signReq = {
  action: RelayMsgAction.SignTransactionRequest,
  time: Math.floor(Date.now() / 1000),
  sequence: manager.nextSequence(),
  transaction: { transaction: txHex, sourceOutputs, broadcast: false },
  inputPaths: [[0, "receive", 0]],
};

The wallet deserializes using toUint8Array and toBigInt on the received fields.