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.