Add serialization helpers to @wizardconnect/core
Canonical encoding for BigInt (<bigint: Xn>) and Uint8Array (hex or <Uint8Array: 0x...>) used in the relay protocol. Provides both serialization (sourceOutputToRelay, transactionToHex) and deserialization (parseExtendedJson, toUint8Array, toBigInt) so dapps and wallets don't have to implement this independently.
This commit is contained in:
parent
a22d98a6b2
commit
889dd566d1
8 changed files with 547 additions and 0 deletions
|
|
@ -67,6 +67,7 @@ Protocol and architecture documentation lives in `docs/`. Keep it up to date whe
|
||||||
| `docs/react.md` | React components, hooks, or QR dialog API changes |
|
| `docs/react.md` | React components, hooks, or QR dialog API changes |
|
||||||
| `docs/pubkey-derivation.md` | xpub delivery, `DappPubkeyStateManager`, or gap-fill logic changes |
|
| `docs/pubkey-derivation.md` | xpub delivery, `DappPubkeyStateManager`, or gap-fill logic changes |
|
||||||
| `docs/xpub-sharing.md` | xpub sharing rationale, security model, or comparison with other protocols changes |
|
| `docs/xpub-sharing.md` | xpub sharing rationale, security model, or comparison with other protocols changes |
|
||||||
|
| `docs/serialization.md` | Relay JSON encoding (`sourceOutputToRelay`, `toBigInt`, `toUint8Array`, etc.) changes |
|
||||||
| `docs/index.md` | New top-level docs files are added |
|
| `docs/index.md` | New top-level docs files are added |
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
|
||||||
132
docs/serialization.md
Normal file
132
docs/serialization.md
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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.
|
||||||
|
|
@ -8,6 +8,16 @@
|
||||||
"url": "https://gitlab.com/riftenlabs/lib/wizardconnect",
|
"url": "https://gitlab.com/riftenlabs/lib/wizardconnect",
|
||||||
"directory": "packages/core"
|
"directory": "packages/core"
|
||||||
},
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./hdwalletv1-serialize": {
|
||||||
|
"types": "./dist/protocols/hdwalletv1-serialize.d.ts",
|
||||||
|
"default": "./dist/protocols/hdwalletv1-serialize.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,10 @@ export {
|
||||||
binToBech32Padded,
|
binToBech32Padded,
|
||||||
bech32PaddedToBin,
|
bech32PaddedToBin,
|
||||||
} from "@bitauth/libauth";
|
} from "@bitauth/libauth";
|
||||||
|
export {
|
||||||
|
parseExtendedJson,
|
||||||
|
parseExtendedJsonValue,
|
||||||
|
isExtendedJsonFormat,
|
||||||
|
toUint8Array,
|
||||||
|
toBigInt,
|
||||||
|
} from "./serialize.js";
|
||||||
|
|
|
||||||
114
packages/core/src/protocols/hdwalletv1-serialize.test.ts
Normal file
114
packages/core/src/protocols/hdwalletv1-serialize.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright (C) 2026 Whiterun LLC,
|
||||||
|
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { hexToBin } from "@bitauth/libauth";
|
||||||
|
import {
|
||||||
|
sourceOutputToRelay,
|
||||||
|
transactionToHex,
|
||||||
|
} from "./hdwalletv1-serialize.js";
|
||||||
|
import { toUint8Array, toBigInt } from "../serialize.js";
|
||||||
|
|
||||||
|
describe("hdwalletv1-serialize", () => {
|
||||||
|
describe("sourceOutputToRelay", () => {
|
||||||
|
it("converts native types to relay-safe JSON", () => {
|
||||||
|
const so = {
|
||||||
|
outpointTransactionHash: hexToBin("ab".repeat(32)),
|
||||||
|
outpointIndex: 0,
|
||||||
|
unlockingBytecode: new Uint8Array(0),
|
||||||
|
sequenceNumber: 0xffffffff,
|
||||||
|
valueSatoshis: 200000n,
|
||||||
|
lockingBytecode: hexToBin("76a914" + "00".repeat(20) + "88ac"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = sourceOutputToRelay(so);
|
||||||
|
|
||||||
|
expect(result.outpointTransactionHash).toBe("ab".repeat(32));
|
||||||
|
expect(result.outpointIndex).toBe(0);
|
||||||
|
expect(result.unlockingBytecode).toBe("");
|
||||||
|
expect(result.sequenceNumber).toBe(0xffffffff);
|
||||||
|
expect(result.valueSatoshis).toBe("<bigint: 200000n>");
|
||||||
|
expect(result.lockingBytecode).toBe("76a914" + "00".repeat(20) + "88ac");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is JSON-serializable", () => {
|
||||||
|
const so = {
|
||||||
|
outpointTransactionHash: new Uint8Array(32),
|
||||||
|
outpointIndex: 0,
|
||||||
|
unlockingBytecode: new Uint8Array(0),
|
||||||
|
sequenceNumber: 0xffffffff,
|
||||||
|
valueSatoshis: 100000000n,
|
||||||
|
lockingBytecode: new Uint8Array([0x76, 0xa9]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = sourceOutputToRelay(so);
|
||||||
|
expect(() => JSON.stringify(result)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes token data when present", () => {
|
||||||
|
const so = {
|
||||||
|
outpointTransactionHash: new Uint8Array(32),
|
||||||
|
outpointIndex: 0,
|
||||||
|
unlockingBytecode: new Uint8Array(0),
|
||||||
|
sequenceNumber: 0xffffffff,
|
||||||
|
valueSatoshis: 546n,
|
||||||
|
lockingBytecode: new Uint8Array([0x76, 0xa9]),
|
||||||
|
token: {
|
||||||
|
category: hexToBin("cc".repeat(32)),
|
||||||
|
amount: 1000n,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = sourceOutputToRelay(so);
|
||||||
|
expect(result.token).toBeDefined();
|
||||||
|
expect(result.token.category).toBe("cc".repeat(32));
|
||||||
|
expect(result.token.amount).toBe("<bigint: 1000n>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips through toBigInt/toUint8Array", () => {
|
||||||
|
const so = {
|
||||||
|
outpointTransactionHash: hexToBin("ab".repeat(32)),
|
||||||
|
outpointIndex: 2,
|
||||||
|
unlockingBytecode: new Uint8Array(0),
|
||||||
|
sequenceNumber: 0xffffffff,
|
||||||
|
valueSatoshis: 200000n,
|
||||||
|
lockingBytecode: hexToBin("76a914" + "00".repeat(20) + "88ac"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const relay = sourceOutputToRelay(so);
|
||||||
|
|
||||||
|
// Simulate what the wallet does: parse the relay JSON
|
||||||
|
expect(toUint8Array(relay.outpointTransactionHash)).toEqual(
|
||||||
|
so.outpointTransactionHash,
|
||||||
|
);
|
||||||
|
expect(toBigInt(relay.valueSatoshis)).toBe(so.valueSatoshis);
|
||||||
|
expect(toUint8Array(relay.lockingBytecode)).toEqual(so.lockingBytecode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("transactionToHex", () => {
|
||||||
|
it("encodes a simple transaction", () => {
|
||||||
|
const inputs = [
|
||||||
|
{
|
||||||
|
outpointTransactionHash: new Uint8Array(32),
|
||||||
|
outpointIndex: 0,
|
||||||
|
unlockingBytecode: new Uint8Array(0),
|
||||||
|
sequenceNumber: 0xffffffff,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const outputs = [
|
||||||
|
{
|
||||||
|
valueSatoshis: 0n,
|
||||||
|
lockingBytecode: new Uint8Array([0x6a]), // OP_RETURN
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const hex = transactionToHex(inputs, outputs);
|
||||||
|
expect(typeof hex).toBe("string");
|
||||||
|
expect(hex.length).toBeGreaterThan(0);
|
||||||
|
// Should start with version 2 in little-endian
|
||||||
|
expect(hex.startsWith("02000000")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
90
packages/core/src/protocols/hdwalletv1-serialize.ts
Normal file
90
packages/core/src/protocols/hdwalletv1-serialize.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright (C) 2026 Whiterun LLC,
|
||||||
|
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialization helpers specific to the hdwalletv1 sign-transaction flow.
|
||||||
|
*
|
||||||
|
* These convert between native libauth transaction types and the relay-safe
|
||||||
|
* JSON used in SignTransactionRequest / SignTransactionResponse messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { binToHex, encodeTransaction } from "@bitauth/libauth";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Interfaces (mirror libauth transaction shapes)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface SourceOutput {
|
||||||
|
outpointTransactionHash: Uint8Array;
|
||||||
|
outpointIndex: number;
|
||||||
|
unlockingBytecode: Uint8Array;
|
||||||
|
sequenceNumber: number;
|
||||||
|
valueSatoshis: bigint;
|
||||||
|
lockingBytecode: Uint8Array;
|
||||||
|
token?: {
|
||||||
|
category: Uint8Array;
|
||||||
|
amount: bigint;
|
||||||
|
nft?: { capability?: string; commitment?: Uint8Array };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TxInput {
|
||||||
|
outpointTransactionHash: Uint8Array;
|
||||||
|
outpointIndex: number;
|
||||||
|
unlockingBytecode: Uint8Array;
|
||||||
|
sequenceNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TxOutput {
|
||||||
|
valueSatoshis: bigint;
|
||||||
|
lockingBytecode: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Serialization (native types → relay JSON)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a source output to relay-safe JSON format.
|
||||||
|
* Uint8Array fields become hex strings, BigInt becomes `<bigint: Xn>`.
|
||||||
|
*/
|
||||||
|
export function sourceOutputToRelay(so: SourceOutput): any {
|
||||||
|
const result: any = {
|
||||||
|
outpointTransactionHash: binToHex(so.outpointTransactionHash),
|
||||||
|
outpointIndex: so.outpointIndex,
|
||||||
|
unlockingBytecode: binToHex(so.unlockingBytecode),
|
||||||
|
sequenceNumber: so.sequenceNumber,
|
||||||
|
valueSatoshis: `<bigint: ${so.valueSatoshis}n>`,
|
||||||
|
lockingBytecode: binToHex(so.lockingBytecode),
|
||||||
|
};
|
||||||
|
if (so.token) {
|
||||||
|
result.token = {
|
||||||
|
category: binToHex(so.token.category),
|
||||||
|
amount: `<bigint: ${so.token.amount}n>`,
|
||||||
|
...(so.token.nft && {
|
||||||
|
nft: {
|
||||||
|
...(so.token.nft.capability !== undefined && {
|
||||||
|
capability: so.token.nft.capability,
|
||||||
|
}),
|
||||||
|
...(so.token.nft.commitment !== undefined && {
|
||||||
|
commitment: binToHex(so.token.nft.commitment),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a transaction to hex for relay transport.
|
||||||
|
*/
|
||||||
|
export function transactionToHex(
|
||||||
|
inputs: TxInput[],
|
||||||
|
outputs: TxOutput[],
|
||||||
|
version = 2,
|
||||||
|
locktime = 0,
|
||||||
|
): string {
|
||||||
|
return binToHex(encodeTransaction({ inputs, outputs, version, locktime }));
|
||||||
|
}
|
||||||
101
packages/core/src/serialize.test.ts
Normal file
101
packages/core/src/serialize.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright (C) 2026 Whiterun LLC,
|
||||||
|
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { binToHex } from "@bitauth/libauth";
|
||||||
|
import {
|
||||||
|
parseExtendedJson,
|
||||||
|
parseExtendedJsonValue,
|
||||||
|
isExtendedJsonFormat,
|
||||||
|
toUint8Array,
|
||||||
|
toBigInt,
|
||||||
|
} from "./serialize.js";
|
||||||
|
|
||||||
|
describe("wc-serialize", () => {
|
||||||
|
describe("parseExtendedJsonValue", () => {
|
||||||
|
it("parses bigint format", () => {
|
||||||
|
expect(parseExtendedJsonValue("<bigint: 546n>")).toBe(546n);
|
||||||
|
expect(parseExtendedJsonValue("<bigint: 0n>")).toBe(0n);
|
||||||
|
expect(parseExtendedJsonValue("<bigint: 100000000n>")).toBe(100000000n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses Uint8Array format", () => {
|
||||||
|
const result = parseExtendedJsonValue("<Uint8Array: 0xabcd>");
|
||||||
|
expect(result).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(binToHex(result as Uint8Array)).toBe("abcd");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns original string for non-extended values", () => {
|
||||||
|
expect(parseExtendedJsonValue("hello")).toBe("hello");
|
||||||
|
expect(parseExtendedJsonValue("123")).toBe("123");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isExtendedJsonFormat", () => {
|
||||||
|
it("detects bigint format", () => {
|
||||||
|
expect(isExtendedJsonFormat("<bigint: 100n>")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects Uint8Array format", () => {
|
||||||
|
expect(isExtendedJsonFormat("<Uint8Array: 0xabcd>")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects plain strings", () => {
|
||||||
|
expect(isExtendedJsonFormat("hello")).toBe(false);
|
||||||
|
expect(isExtendedJsonFormat("123")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseExtendedJson", () => {
|
||||||
|
it("parses JSON with mixed extended values", () => {
|
||||||
|
const json = JSON.stringify({
|
||||||
|
value: "<bigint: 200000n>",
|
||||||
|
data: "<Uint8Array: 0x76a9>",
|
||||||
|
name: "test",
|
||||||
|
count: 42,
|
||||||
|
});
|
||||||
|
const result = parseExtendedJson(json);
|
||||||
|
expect(result.value).toBe(200000n);
|
||||||
|
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(binToHex(result.data)).toBe("76a9");
|
||||||
|
expect(result.name).toBe("test");
|
||||||
|
expect(result.count).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toUint8Array", () => {
|
||||||
|
it("passes through Uint8Array", () => {
|
||||||
|
const bytes = new Uint8Array([1, 2, 3]);
|
||||||
|
expect(toUint8Array(bytes)).toBe(bytes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts hex string", () => {
|
||||||
|
const result = toUint8Array("abcd");
|
||||||
|
expect(binToHex(result)).toBe("abcd");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts extended JSON format", () => {
|
||||||
|
const result = toUint8Array("<Uint8Array: 0xabcd>");
|
||||||
|
expect(binToHex(result)).toBe("abcd");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toBigInt", () => {
|
||||||
|
it("passes through bigint", () => {
|
||||||
|
expect(toBigInt(546n)).toBe(546n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts number", () => {
|
||||||
|
expect(toBigInt(546)).toBe(546n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts numeric string", () => {
|
||||||
|
expect(toBigInt("546")).toBe(546n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts extended JSON format", () => {
|
||||||
|
expect(toBigInt("<bigint: 546n>")).toBe(546n);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
92
packages/core/src/serialize.ts
Normal file
92
packages/core/src/serialize.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright (C) 2026 Whiterun LLC,
|
||||||
|
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialization helpers for converting between native libauth types and the
|
||||||
|
* relay-safe JSON format used by the WizardConnect protocol.
|
||||||
|
*
|
||||||
|
* The relay transmits messages as JSON, which cannot represent BigInt or
|
||||||
|
* Uint8Array natively. This module provides the canonical encoding:
|
||||||
|
*
|
||||||
|
* - Uint8Array → hex string (or `<Uint8Array: 0x...>` extended format)
|
||||||
|
* - BigInt → `<bigint: Xn>` string
|
||||||
|
*
|
||||||
|
* Both dapps and wallets should use these helpers to ensure interoperability.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { hexToBin } from "@bitauth/libauth";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Deserialization (relay JSON → native types)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const BIGINT_RE = /^<bigint: (?<bigint>[0-9]*)n>$/;
|
||||||
|
const UINT8_RE = /^<Uint8Array: 0x(?<hex>[0-9a-f]*)>$/u;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a full JSON string that may contain extended-format values.
|
||||||
|
* Handles both `<Uint8Array: 0x...>` and `<bigint: ...n>` formats.
|
||||||
|
*/
|
||||||
|
export function parseExtendedJson(jsonString: string): any {
|
||||||
|
return JSON.parse(jsonString, (_key, value) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const bigintMatch = value.match(BIGINT_RE);
|
||||||
|
if (bigintMatch) return BigInt(bigintMatch[1]);
|
||||||
|
const uint8Match = value.match(UINT8_RE);
|
||||||
|
if (uint8Match) return hexToBin(uint8Match[1]);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string contains extended JSON markers.
|
||||||
|
*/
|
||||||
|
export function isExtendedJsonFormat(str: string): boolean {
|
||||||
|
return UINT8_RE.test(str) || BIGINT_RE.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single extended JSON value string to its native type.
|
||||||
|
* Returns the original string if it doesn't match any known format.
|
||||||
|
*/
|
||||||
|
export function parseExtendedJsonValue(
|
||||||
|
value: string,
|
||||||
|
): Uint8Array | bigint | string {
|
||||||
|
const bigintMatch = value.match(BIGINT_RE);
|
||||||
|
if (bigintMatch) return BigInt(bigintMatch[1]);
|
||||||
|
const uint8Match = value.match(UINT8_RE);
|
||||||
|
if (uint8Match) return hexToBin(uint8Match[1]);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value to Uint8Array. Accepts:
|
||||||
|
* - Uint8Array (returned as-is)
|
||||||
|
* - hex string
|
||||||
|
* - extended JSON format string (`<Uint8Array: 0x...>`)
|
||||||
|
*/
|
||||||
|
export function toUint8Array(value: string | Uint8Array): Uint8Array {
|
||||||
|
if (value instanceof Uint8Array) return value;
|
||||||
|
if (isExtendedJsonFormat(value))
|
||||||
|
return parseExtendedJsonValue(value) as Uint8Array;
|
||||||
|
return hexToBin(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value to bigint. Accepts:
|
||||||
|
* - bigint (returned as-is)
|
||||||
|
* - number
|
||||||
|
* - numeric string
|
||||||
|
* - extended JSON format string (`<bigint: Xn>`)
|
||||||
|
*/
|
||||||
|
export function toBigInt(value: string | number | bigint): bigint {
|
||||||
|
if (typeof value === "bigint") return value;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (isExtendedJsonFormat(value))
|
||||||
|
return parseExtendedJsonValue(value) as bigint;
|
||||||
|
return BigInt(value);
|
||||||
|
}
|
||||||
|
return BigInt(value);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue