@qubic.ts/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +37 -0
- package/scripts/verify-browser.mjs +15 -0
- package/scripts/verify-node.mjs +11 -0
- package/src/crypto/k12.ts +13 -0
- package/src/crypto/schnorrq.ts +3 -0
- package/src/crypto/seed.test.ts +11 -0
- package/src/crypto/seed.ts +63 -0
- package/src/index.browser.ts +85 -0
- package/src/index.node.ts +2 -0
- package/src/index.ts +85 -0
- package/src/primitives/identity.test.ts +41 -0
- package/src/primitives/identity.ts +108 -0
- package/src/primitives/number64.ts +58 -0
- package/src/protocol/assets.test.ts +86 -0
- package/src/protocol/assets.ts +276 -0
- package/src/protocol/broadcast-transaction.test.ts +34 -0
- package/src/protocol/broadcast-transaction.ts +17 -0
- package/src/protocol/contract-function.test.ts +37 -0
- package/src/protocol/contract-function.ts +53 -0
- package/src/protocol/entity.test.ts +58 -0
- package/src/protocol/entity.ts +82 -0
- package/src/protocol/message-types.ts +16 -0
- package/src/protocol/packet-framer.test.ts +101 -0
- package/src/protocol/packet-framer.ts +120 -0
- package/src/protocol/request-packet.ts +48 -0
- package/src/protocol/request-response-header.test.ts +69 -0
- package/src/protocol/request-response-header.ts +56 -0
- package/src/protocol/stream.test.ts +74 -0
- package/src/protocol/stream.ts +48 -0
- package/src/protocol/system-info.test.ts +76 -0
- package/src/protocol/system-info.ts +111 -0
- package/src/protocol/tick-data.test.ts +63 -0
- package/src/protocol/tick-data.ts +127 -0
- package/src/protocol/tick-info.test.ts +35 -0
- package/src/protocol/tick-info.ts +34 -0
- package/src/transactions/transaction.test.ts +80 -0
- package/src/transactions/transaction.ts +150 -0
- package/src/transport/async-queue.ts +60 -0
- package/src/transport/bridge.test.ts +114 -0
- package/src/transport/bridge.ts +178 -0
- package/src/transport/tcp.test.ts +99 -0
- package/src/transport/tcp.ts +134 -0
- package/src/transport/transport.ts +5 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { readI64LE, readU64LE, writeU64LE } from "../primitives/number64.js";
|
|
2
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
3
|
+
import { encodeRequestPacket } from "./request-packet.js";
|
|
4
|
+
|
|
5
|
+
export const ASSETS_DEPTH = 24;
|
|
6
|
+
export const ASSET_RECORD_SIZE = 48;
|
|
7
|
+
export const RESPOND_ASSETS_PAYLOAD_SIZE = 56;
|
|
8
|
+
export const RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE = 824;
|
|
9
|
+
|
|
10
|
+
export const AssetRecordType = {
|
|
11
|
+
EMPTY: 0,
|
|
12
|
+
ISSUANCE: 1,
|
|
13
|
+
OWNERSHIP: 2,
|
|
14
|
+
POSSESSION: 3,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type IssuanceAssetRecord = Readonly<{
|
|
18
|
+
type: typeof AssetRecordType.ISSUANCE;
|
|
19
|
+
publicKey32: Uint8Array;
|
|
20
|
+
name7: Uint8Array;
|
|
21
|
+
numberOfDecimalPlaces: number; // int8-ish
|
|
22
|
+
unitOfMeasurement7: Uint8Array;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
export type OwnershipAssetRecord = Readonly<{
|
|
26
|
+
type: typeof AssetRecordType.OWNERSHIP;
|
|
27
|
+
publicKey32: Uint8Array;
|
|
28
|
+
managingContractIndex: number; // uint16
|
|
29
|
+
issuanceIndex: number; // uint32
|
|
30
|
+
numberOfShares: bigint; // int64
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export type PossessionAssetRecord = Readonly<{
|
|
34
|
+
type: typeof AssetRecordType.POSSESSION;
|
|
35
|
+
publicKey32: Uint8Array;
|
|
36
|
+
managingContractIndex: number; // uint16
|
|
37
|
+
ownershipIndex: number; // uint32
|
|
38
|
+
numberOfShares: bigint; // int64
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
export type AssetRecord =
|
|
42
|
+
| IssuanceAssetRecord
|
|
43
|
+
| OwnershipAssetRecord
|
|
44
|
+
| PossessionAssetRecord
|
|
45
|
+
| Readonly<{ type: typeof AssetRecordType.EMPTY; publicKey32: Uint8Array }>;
|
|
46
|
+
|
|
47
|
+
export type RespondAssets = Readonly<{
|
|
48
|
+
asset: AssetRecord;
|
|
49
|
+
tick: number; // uint32
|
|
50
|
+
universeIndex: number; // uint32
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
export type RespondAssetsWithSiblings = Readonly<
|
|
54
|
+
RespondAssets & { siblings: ReadonlyArray<Uint8Array> }
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
function assertUint8ArrayLength(bytes: Uint8Array, length: number, name: string) {
|
|
58
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
59
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
60
|
+
}
|
|
61
|
+
if (bytes.byteLength !== length) {
|
|
62
|
+
throw new RangeError(`${name} must be ${length} bytes`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertU32(value: number, name: string) {
|
|
67
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
68
|
+
throw new RangeError(`${name} must be a uint32`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertU16(value: number, name: string) {
|
|
73
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff) {
|
|
74
|
+
throw new RangeError(`${name} must be a uint16`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const RequestAssetsType = {
|
|
79
|
+
ISSUANCE: 0,
|
|
80
|
+
OWNERSHIP: 1,
|
|
81
|
+
POSSESSION: 2,
|
|
82
|
+
BY_UNIVERSE_INDEX: 3,
|
|
83
|
+
} as const;
|
|
84
|
+
|
|
85
|
+
export const RequestAssetsFlag = {
|
|
86
|
+
GET_SIBLINGS: 0b1,
|
|
87
|
+
ANY_ISSUER: 0b10,
|
|
88
|
+
ANY_ASSET_NAME: 0b100,
|
|
89
|
+
ANY_OWNER: 0b1000,
|
|
90
|
+
ANY_OWNERSHIP_MANAGING_CONTRACT: 0b10000,
|
|
91
|
+
ANY_POSSESSOR: 0b100000,
|
|
92
|
+
ANY_POSSESSION_MANAGING_CONTRACT: 0b1000000,
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
export type RequestAssetsByFilterParams = Readonly<{
|
|
96
|
+
requestType:
|
|
97
|
+
| typeof RequestAssetsType.ISSUANCE
|
|
98
|
+
| typeof RequestAssetsType.OWNERSHIP
|
|
99
|
+
| typeof RequestAssetsType.POSSESSION;
|
|
100
|
+
getSiblings?: boolean;
|
|
101
|
+
|
|
102
|
+
issuerPublicKey32?: Uint8Array;
|
|
103
|
+
assetNameU64LE?: bigint;
|
|
104
|
+
ownerPublicKey32?: Uint8Array;
|
|
105
|
+
possessorPublicKey32?: Uint8Array;
|
|
106
|
+
ownershipManagingContractIndex?: number;
|
|
107
|
+
possessionManagingContractIndex?: number;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
export type RequestAssetsByUniverseIndexParams = Readonly<{
|
|
111
|
+
universeIndex: number; // uint32
|
|
112
|
+
getSiblings?: boolean;
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
export function assetNameToU64LE(name: string): bigint {
|
|
116
|
+
if (typeof name !== "string") {
|
|
117
|
+
throw new TypeError("name must be a string");
|
|
118
|
+
}
|
|
119
|
+
if (name.length > 8) {
|
|
120
|
+
throw new RangeError("name must be <= 8 characters");
|
|
121
|
+
}
|
|
122
|
+
const bytes = new Uint8Array(8);
|
|
123
|
+
for (let i = 0; i < name.length; i++) {
|
|
124
|
+
const c = name.charCodeAt(i);
|
|
125
|
+
if (c < 32 || c > 126) throw new Error("name must be ASCII");
|
|
126
|
+
bytes[i] = c;
|
|
127
|
+
}
|
|
128
|
+
return readU64LE(bytes, 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function encodeRequestAssetsByUniverseIndex(
|
|
132
|
+
params: RequestAssetsByUniverseIndexParams,
|
|
133
|
+
): Uint8Array {
|
|
134
|
+
assertU32(params.universeIndex, "universeIndex");
|
|
135
|
+
|
|
136
|
+
const flags = params.getSiblings ? RequestAssetsFlag.GET_SIBLINGS : 0;
|
|
137
|
+
const payload = new Uint8Array(112);
|
|
138
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
139
|
+
view.setUint16(0, RequestAssetsType.BY_UNIVERSE_INDEX, true);
|
|
140
|
+
view.setUint16(2, flags, true);
|
|
141
|
+
view.setUint32(4, params.universeIndex, true);
|
|
142
|
+
return encodeRequestPacket(NetworkMessageType.REQUEST_ASSETS, payload);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function encodeRequestAssetsByFilter(params: RequestAssetsByFilterParams): Uint8Array {
|
|
146
|
+
const requestType = params.requestType;
|
|
147
|
+
if (
|
|
148
|
+
requestType !== RequestAssetsType.ISSUANCE &&
|
|
149
|
+
requestType !== RequestAssetsType.OWNERSHIP &&
|
|
150
|
+
requestType !== RequestAssetsType.POSSESSION
|
|
151
|
+
) {
|
|
152
|
+
throw new RangeError("requestType is invalid");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let flags = params.getSiblings ? RequestAssetsFlag.GET_SIBLINGS : 0;
|
|
156
|
+
|
|
157
|
+
const ownershipManagingContractIndex = params.ownershipManagingContractIndex ?? 0;
|
|
158
|
+
const possessionManagingContractIndex = params.possessionManagingContractIndex ?? 0;
|
|
159
|
+
assertU16(ownershipManagingContractIndex, "ownershipManagingContractIndex");
|
|
160
|
+
assertU16(possessionManagingContractIndex, "possessionManagingContractIndex");
|
|
161
|
+
|
|
162
|
+
const issuerPublicKey32 = params.issuerPublicKey32 ?? new Uint8Array(32);
|
|
163
|
+
const ownerPublicKey32 = params.ownerPublicKey32 ?? new Uint8Array(32);
|
|
164
|
+
const possessorPublicKey32 = params.possessorPublicKey32 ?? new Uint8Array(32);
|
|
165
|
+
|
|
166
|
+
if (params.issuerPublicKey32 === undefined) flags |= RequestAssetsFlag.ANY_ISSUER;
|
|
167
|
+
if (params.assetNameU64LE === undefined) flags |= RequestAssetsFlag.ANY_ASSET_NAME;
|
|
168
|
+
if (requestType === RequestAssetsType.OWNERSHIP && params.ownerPublicKey32 === undefined)
|
|
169
|
+
flags |= RequestAssetsFlag.ANY_OWNER;
|
|
170
|
+
if (
|
|
171
|
+
requestType === RequestAssetsType.OWNERSHIP &&
|
|
172
|
+
params.ownershipManagingContractIndex === undefined
|
|
173
|
+
)
|
|
174
|
+
flags |= RequestAssetsFlag.ANY_OWNERSHIP_MANAGING_CONTRACT;
|
|
175
|
+
if (requestType === RequestAssetsType.POSSESSION && params.possessorPublicKey32 === undefined)
|
|
176
|
+
flags |= RequestAssetsFlag.ANY_POSSESSOR;
|
|
177
|
+
if (
|
|
178
|
+
requestType === RequestAssetsType.POSSESSION &&
|
|
179
|
+
params.possessionManagingContractIndex === undefined
|
|
180
|
+
)
|
|
181
|
+
flags |= RequestAssetsFlag.ANY_POSSESSION_MANAGING_CONTRACT;
|
|
182
|
+
|
|
183
|
+
assertUint8ArrayLength(issuerPublicKey32, 32, "issuerPublicKey32");
|
|
184
|
+
assertUint8ArrayLength(ownerPublicKey32, 32, "ownerPublicKey32");
|
|
185
|
+
assertUint8ArrayLength(possessorPublicKey32, 32, "possessorPublicKey32");
|
|
186
|
+
|
|
187
|
+
const assetNameU64LE = params.assetNameU64LE ?? 0n;
|
|
188
|
+
if (assetNameU64LE < 0n || assetNameU64LE > 0xffff_ffff_ffff_ffffn) {
|
|
189
|
+
throw new RangeError("assetNameU64LE must fit in uint64");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const payload = new Uint8Array(112);
|
|
193
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
194
|
+
view.setUint16(0, requestType, true);
|
|
195
|
+
view.setUint16(2, flags, true);
|
|
196
|
+
view.setUint16(4, ownershipManagingContractIndex, true);
|
|
197
|
+
view.setUint16(6, possessionManagingContractIndex, true);
|
|
198
|
+
payload.set(issuerPublicKey32, 8);
|
|
199
|
+
writeU64LE(assetNameU64LE, payload, 40);
|
|
200
|
+
payload.set(ownerPublicKey32, 48);
|
|
201
|
+
payload.set(possessorPublicKey32, 80);
|
|
202
|
+
|
|
203
|
+
return encodeRequestPacket(NetworkMessageType.REQUEST_ASSETS, payload);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function decodeAssetRecord(recordBytes48: Uint8Array): AssetRecord {
|
|
207
|
+
assertUint8ArrayLength(recordBytes48, ASSET_RECORD_SIZE, "recordBytes48");
|
|
208
|
+
const type = recordBytes48[32] ?? 0;
|
|
209
|
+
const view = new DataView(
|
|
210
|
+
recordBytes48.buffer,
|
|
211
|
+
recordBytes48.byteOffset,
|
|
212
|
+
recordBytes48.byteLength,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (type === AssetRecordType.ISSUANCE) {
|
|
216
|
+
return {
|
|
217
|
+
type: AssetRecordType.ISSUANCE,
|
|
218
|
+
publicKey32: recordBytes48.slice(0, 32),
|
|
219
|
+
name7: recordBytes48.slice(33, 40),
|
|
220
|
+
numberOfDecimalPlaces: ((recordBytes48[40] ?? 0) << 24) >> 24,
|
|
221
|
+
unitOfMeasurement7: recordBytes48.slice(41, 48),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (type === AssetRecordType.OWNERSHIP) {
|
|
226
|
+
return {
|
|
227
|
+
type: AssetRecordType.OWNERSHIP,
|
|
228
|
+
publicKey32: recordBytes48.slice(0, 32),
|
|
229
|
+
managingContractIndex: view.getUint16(34, true),
|
|
230
|
+
issuanceIndex: view.getUint32(36, true),
|
|
231
|
+
numberOfShares: readI64LE(recordBytes48, 40),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (type === AssetRecordType.POSSESSION) {
|
|
236
|
+
return {
|
|
237
|
+
type: AssetRecordType.POSSESSION,
|
|
238
|
+
publicKey32: recordBytes48.slice(0, 32),
|
|
239
|
+
managingContractIndex: view.getUint16(34, true),
|
|
240
|
+
ownershipIndex: view.getUint32(36, true),
|
|
241
|
+
numberOfShares: readI64LE(recordBytes48, 40),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { type: AssetRecordType.EMPTY, publicKey32: recordBytes48.slice(0, 32) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function decodeRespondAssets(payload: Uint8Array): RespondAssets {
|
|
249
|
+
if (!(payload instanceof Uint8Array)) throw new TypeError("payload must be a Uint8Array");
|
|
250
|
+
if (payload.byteLength !== RESPOND_ASSETS_PAYLOAD_SIZE) {
|
|
251
|
+
throw new RangeError(`payload must be ${RESPOND_ASSETS_PAYLOAD_SIZE} bytes`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
255
|
+
const asset = decodeAssetRecord(payload.subarray(0, ASSET_RECORD_SIZE));
|
|
256
|
+
const tick = view.getUint32(48, true);
|
|
257
|
+
const universeIndex = view.getUint32(52, true);
|
|
258
|
+
|
|
259
|
+
return { asset, tick, universeIndex };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function decodeRespondAssetsWithSiblings(payload: Uint8Array): RespondAssetsWithSiblings {
|
|
263
|
+
if (!(payload instanceof Uint8Array)) throw new TypeError("payload must be a Uint8Array");
|
|
264
|
+
if (payload.byteLength !== RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE) {
|
|
265
|
+
throw new RangeError(`payload must be ${RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE} bytes`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const base = decodeRespondAssets(payload.subarray(0, RESPOND_ASSETS_PAYLOAD_SIZE));
|
|
269
|
+
const siblings: Uint8Array[] = [];
|
|
270
|
+
const siblingsOffset = RESPOND_ASSETS_PAYLOAD_SIZE;
|
|
271
|
+
for (let i = 0; i < ASSETS_DEPTH; i++) {
|
|
272
|
+
const start = siblingsOffset + i * 32;
|
|
273
|
+
siblings.push(payload.slice(start, start + 32));
|
|
274
|
+
}
|
|
275
|
+
return { ...base, siblings };
|
|
276
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { privateKeyFromSeed } from "../crypto/seed.js";
|
|
3
|
+
import { publicKeyFromIdentity } from "../primitives/identity.js";
|
|
4
|
+
import { buildSignedTransaction } from "../transactions/transaction.js";
|
|
5
|
+
import { encodeBroadcastTransactionPacket } from "./broadcast-transaction.js";
|
|
6
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
7
|
+
import { decodeRequestResponseHeader } from "./request-response-header.js";
|
|
8
|
+
|
|
9
|
+
describe("broadcast-transaction", () => {
|
|
10
|
+
it("encodes a BROADCAST_TRANSACTION packet with dejavu=0", async () => {
|
|
11
|
+
const seed = "jvhbyzjinlyutyuhsweuxiwootqoevjqwqmdhjeohrytxjxidpbcfyg";
|
|
12
|
+
const secretKey32 = await privateKeyFromSeed(seed);
|
|
13
|
+
|
|
14
|
+
const sourcePublicKey32 = publicKeyFromIdentity(
|
|
15
|
+
"HZEBBDSKZRTAWGYMTTSDZQDXYWPBUKBEAIYZNFLVWARZJBEBIJRRFKUDVETA",
|
|
16
|
+
);
|
|
17
|
+
const destinationPublicKey32 = publicKeyFromIdentity(
|
|
18
|
+
"AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const txBytes = await buildSignedTransaction(
|
|
22
|
+
{ sourcePublicKey32, destinationPublicKey32, amount: 1n, tick: 12345 },
|
|
23
|
+
secretKey32,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const packet = encodeBroadcastTransactionPacket(txBytes);
|
|
27
|
+
const header = decodeRequestResponseHeader(packet.subarray(0, 8));
|
|
28
|
+
|
|
29
|
+
expect(header.size).toBe(8 + txBytes.byteLength);
|
|
30
|
+
expect(header.type).toBe(NetworkMessageType.BROADCAST_TRANSACTION);
|
|
31
|
+
expect(header.dejavu).toBe(0);
|
|
32
|
+
expect(packet.subarray(8)).toEqual(txBytes);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MAX_TRANSACTION_SIZE, TRANSACTION_HEADER_SIZE } from "../transactions/transaction.js";
|
|
2
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
3
|
+
import { encodeRequestPacket } from "./request-packet.js";
|
|
4
|
+
|
|
5
|
+
export function encodeBroadcastTransactionPacket(txBytes: Uint8Array): Uint8Array {
|
|
6
|
+
if (!(txBytes instanceof Uint8Array)) {
|
|
7
|
+
throw new TypeError("txBytes must be a Uint8Array");
|
|
8
|
+
}
|
|
9
|
+
if (txBytes.byteLength < TRANSACTION_HEADER_SIZE) {
|
|
10
|
+
throw new RangeError("txBytes is too short");
|
|
11
|
+
}
|
|
12
|
+
if (txBytes.byteLength > MAX_TRANSACTION_SIZE) {
|
|
13
|
+
throw new RangeError(`txBytes must be <= ${MAX_TRANSACTION_SIZE} bytes`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return encodeRequestPacket(NetworkMessageType.BROADCAST_TRANSACTION, txBytes, { dejavu: 0 });
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
decodeRespondContractFunction,
|
|
4
|
+
encodeRequestContractFunction,
|
|
5
|
+
REQUEST_CONTRACT_FUNCTION_PREFIX_SIZE,
|
|
6
|
+
} from "./contract-function.js";
|
|
7
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
8
|
+
import { decodeRequestResponseHeader } from "./request-response-header.js";
|
|
9
|
+
|
|
10
|
+
describe("contract-function", () => {
|
|
11
|
+
it("encodes request contract function packet", () => {
|
|
12
|
+
const packet = encodeRequestContractFunction({
|
|
13
|
+
contractIndex: 123,
|
|
14
|
+
inputType: 42,
|
|
15
|
+
inputBytes: Uint8Array.from([1, 2, 3]),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const header = decodeRequestResponseHeader(packet.subarray(0, 8));
|
|
19
|
+
expect(header.type).toBe(NetworkMessageType.REQUEST_CONTRACT_FUNCTION);
|
|
20
|
+
expect(header.size).toBe(8 + REQUEST_CONTRACT_FUNCTION_PREFIX_SIZE + 3);
|
|
21
|
+
expect(header.dejavu).not.toBe(0);
|
|
22
|
+
|
|
23
|
+
const payload = packet.subarray(8);
|
|
24
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
25
|
+
expect(view.getUint32(0, true)).toBe(123);
|
|
26
|
+
expect(view.getUint16(4, true)).toBe(42);
|
|
27
|
+
expect(view.getUint16(6, true)).toBe(3);
|
|
28
|
+
expect(payload.subarray(REQUEST_CONTRACT_FUNCTION_PREFIX_SIZE)).toEqual(
|
|
29
|
+
Uint8Array.from([1, 2, 3]),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("decodes contract function response payload as raw bytes", () => {
|
|
34
|
+
const payload = Uint8Array.from([9, 8, 7]);
|
|
35
|
+
expect(decodeRespondContractFunction(payload)).toEqual(payload);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
2
|
+
import { encodeRequestPacket } from "./request-packet.js";
|
|
3
|
+
|
|
4
|
+
export const REQUEST_CONTRACT_FUNCTION_PREFIX_SIZE = 8; // u32 + u16 + u16
|
|
5
|
+
|
|
6
|
+
export type RequestContractFunctionParams = Readonly<{
|
|
7
|
+
contractIndex: number; // uint32
|
|
8
|
+
inputType: number; // uint16
|
|
9
|
+
inputBytes: Uint8Array;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
function assertU32(value: number, name: string) {
|
|
13
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
14
|
+
throw new RangeError(`${name} must be a uint32`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertU16(value: number, name: string) {
|
|
19
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff) {
|
|
20
|
+
throw new RangeError(`${name} must be a uint16`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function encodeRequestContractFunction(params: RequestContractFunctionParams): Uint8Array {
|
|
25
|
+
assertU32(params.contractIndex, "contractIndex");
|
|
26
|
+
assertU16(params.inputType, "inputType");
|
|
27
|
+
|
|
28
|
+
if (!(params.inputBytes instanceof Uint8Array)) {
|
|
29
|
+
throw new TypeError("inputBytes must be a Uint8Array");
|
|
30
|
+
}
|
|
31
|
+
if (params.inputBytes.byteLength > 0xffff) {
|
|
32
|
+
throw new RangeError("inputBytes must be <= 65535 bytes");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const payload = new Uint8Array(
|
|
36
|
+
REQUEST_CONTRACT_FUNCTION_PREFIX_SIZE + params.inputBytes.byteLength,
|
|
37
|
+
);
|
|
38
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
39
|
+
view.setUint32(0, params.contractIndex, true);
|
|
40
|
+
view.setUint16(4, params.inputType, true);
|
|
41
|
+
view.setUint16(6, params.inputBytes.byteLength, true);
|
|
42
|
+
payload.set(params.inputBytes, REQUEST_CONTRACT_FUNCTION_PREFIX_SIZE);
|
|
43
|
+
|
|
44
|
+
return encodeRequestPacket(NetworkMessageType.REQUEST_CONTRACT_FUNCTION, payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function decodeRespondContractFunction(payload: Uint8Array): Uint8Array {
|
|
48
|
+
if (!(payload instanceof Uint8Array)) {
|
|
49
|
+
throw new TypeError("payload must be a Uint8Array");
|
|
50
|
+
}
|
|
51
|
+
// Contract response is variable length; empty payload indicates failure.
|
|
52
|
+
return payload.slice();
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
decodeRespondEntity,
|
|
4
|
+
encodeRequestEntity,
|
|
5
|
+
RESPOND_ENTITY_PAYLOAD_SIZE,
|
|
6
|
+
SPECTRUM_DEPTH,
|
|
7
|
+
} from "./entity.js";
|
|
8
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
9
|
+
import { decodeRequestResponseHeader } from "./request-response-header.js";
|
|
10
|
+
|
|
11
|
+
describe("entity", () => {
|
|
12
|
+
it("encodes request entity packet", () => {
|
|
13
|
+
const pub = new Uint8Array(32).fill(7);
|
|
14
|
+
const packet = encodeRequestEntity(pub);
|
|
15
|
+
expect(packet.byteLength).toBe(8 + 32);
|
|
16
|
+
const header = decodeRequestResponseHeader(packet.subarray(0, 8));
|
|
17
|
+
expect(header.size).toBe(40);
|
|
18
|
+
expect(header.type).toBe(NetworkMessageType.REQUEST_ENTITY);
|
|
19
|
+
expect(packet.subarray(8)).toEqual(pub);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("decodes respond entity payload", () => {
|
|
23
|
+
const payload = new Uint8Array(RESPOND_ENTITY_PAYLOAD_SIZE);
|
|
24
|
+
payload.set(
|
|
25
|
+
new Uint8Array(32).map((_, i) => i),
|
|
26
|
+
0,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
30
|
+
view.setBigInt64(32, 100n, true);
|
|
31
|
+
view.setBigInt64(40, 25n, true);
|
|
32
|
+
view.setUint32(48, 3, true);
|
|
33
|
+
view.setUint32(52, 4, true);
|
|
34
|
+
view.setUint32(56, 111, true);
|
|
35
|
+
view.setUint32(60, 222, true);
|
|
36
|
+
view.setUint32(64, 333, true);
|
|
37
|
+
view.setInt32(68, -1, true);
|
|
38
|
+
|
|
39
|
+
const siblingsOffset = 72;
|
|
40
|
+
for (let i = 0; i < SPECTRUM_DEPTH; i++) {
|
|
41
|
+
const sib = new Uint8Array(32).fill(i);
|
|
42
|
+
payload.set(sib, siblingsOffset + i * 32);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const decoded = decodeRespondEntity(payload);
|
|
46
|
+
expect(decoded.tick).toBe(333);
|
|
47
|
+
expect(decoded.spectrumIndex).toBe(-1);
|
|
48
|
+
expect(decoded.entity.incomingAmount).toBe(100n);
|
|
49
|
+
expect(decoded.entity.outgoingAmount).toBe(25n);
|
|
50
|
+
expect(decoded.entity.numberOfIncomingTransfers).toBe(3);
|
|
51
|
+
expect(decoded.entity.numberOfOutgoingTransfers).toBe(4);
|
|
52
|
+
expect(decoded.entity.latestIncomingTransferTick).toBe(111);
|
|
53
|
+
expect(decoded.entity.latestOutgoingTransferTick).toBe(222);
|
|
54
|
+
expect(decoded.siblings).toHaveLength(SPECTRUM_DEPTH);
|
|
55
|
+
expect(decoded.siblings[0]).toEqual(new Uint8Array(32).fill(0));
|
|
56
|
+
expect(decoded.siblings[23]).toEqual(new Uint8Array(32).fill(23));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readI64LE } from "../primitives/number64.js";
|
|
2
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
3
|
+
import { encodeRequestPacket } from "./request-packet.js";
|
|
4
|
+
|
|
5
|
+
export const SPECTRUM_DEPTH = 24;
|
|
6
|
+
export const ENTITY_RECORD_SIZE = 64;
|
|
7
|
+
export const RESPOND_ENTITY_PAYLOAD_SIZE = ENTITY_RECORD_SIZE + 4 + 4 + 32 * SPECTRUM_DEPTH; // 840
|
|
8
|
+
|
|
9
|
+
export type EntityRecord = Readonly<{
|
|
10
|
+
publicKey32: Uint8Array;
|
|
11
|
+
incomingAmount: bigint; // int64
|
|
12
|
+
outgoingAmount: bigint; // int64
|
|
13
|
+
numberOfIncomingTransfers: number; // uint32
|
|
14
|
+
numberOfOutgoingTransfers: number; // uint32
|
|
15
|
+
latestIncomingTransferTick: number; // uint32
|
|
16
|
+
latestOutgoingTransferTick: number; // uint32
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type RespondEntity = Readonly<{
|
|
20
|
+
entity: EntityRecord;
|
|
21
|
+
tick: number; // uint32
|
|
22
|
+
spectrumIndex: number; // int32
|
|
23
|
+
siblings: ReadonlyArray<Uint8Array>; // [SPECTRUM_DEPTH][32]
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
function assertUint8ArrayLength(bytes: Uint8Array, length: number, name: string) {
|
|
27
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
28
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
29
|
+
}
|
|
30
|
+
if (bytes.byteLength !== length) {
|
|
31
|
+
throw new RangeError(`${name} must be ${length} bytes`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function encodeRequestEntity(publicKey32: Uint8Array): Uint8Array {
|
|
36
|
+
assertUint8ArrayLength(publicKey32, 32, "publicKey32");
|
|
37
|
+
return encodeRequestPacket(NetworkMessageType.REQUEST_ENTITY, publicKey32);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decodeRespondEntity(payload: Uint8Array): RespondEntity {
|
|
41
|
+
if (!(payload instanceof Uint8Array)) {
|
|
42
|
+
throw new TypeError("payload must be a Uint8Array");
|
|
43
|
+
}
|
|
44
|
+
if (payload.byteLength !== RESPOND_ENTITY_PAYLOAD_SIZE) {
|
|
45
|
+
throw new RangeError(`payload must be ${RESPOND_ENTITY_PAYLOAD_SIZE} bytes`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
49
|
+
|
|
50
|
+
const publicKey32 = payload.subarray(0, 32);
|
|
51
|
+
const incomingAmount = readI64LE(payload, 32);
|
|
52
|
+
const outgoingAmount = readI64LE(payload, 40);
|
|
53
|
+
const numberOfIncomingTransfers = view.getUint32(48, true);
|
|
54
|
+
const numberOfOutgoingTransfers = view.getUint32(52, true);
|
|
55
|
+
const latestIncomingTransferTick = view.getUint32(56, true);
|
|
56
|
+
const latestOutgoingTransferTick = view.getUint32(60, true);
|
|
57
|
+
|
|
58
|
+
const tick = view.getUint32(64, true);
|
|
59
|
+
const spectrumIndex = view.getInt32(68, true);
|
|
60
|
+
|
|
61
|
+
const siblingsOffset = 72;
|
|
62
|
+
const siblings: Uint8Array[] = [];
|
|
63
|
+
for (let i = 0; i < SPECTRUM_DEPTH; i++) {
|
|
64
|
+
const start = siblingsOffset + i * 32;
|
|
65
|
+
siblings.push(payload.slice(start, start + 32));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
entity: {
|
|
70
|
+
publicKey32: publicKey32.slice(),
|
|
71
|
+
incomingAmount,
|
|
72
|
+
outgoingAmount,
|
|
73
|
+
numberOfIncomingTransfers,
|
|
74
|
+
numberOfOutgoingTransfers,
|
|
75
|
+
latestIncomingTransferTick,
|
|
76
|
+
latestOutgoingTransferTick,
|
|
77
|
+
},
|
|
78
|
+
tick,
|
|
79
|
+
spectrumIndex,
|
|
80
|
+
siblings,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const NetworkMessageType = {
|
|
2
|
+
BROADCAST_FUTURE_TICK_DATA: 8,
|
|
3
|
+
REQUEST_TICK_DATA: 16,
|
|
4
|
+
REQUEST_CONTRACT_FUNCTION: 42,
|
|
5
|
+
RESPOND_CONTRACT_FUNCTION: 43,
|
|
6
|
+
REQUEST_CURRENT_TICK_INFO: 27,
|
|
7
|
+
RESPOND_CURRENT_TICK_INFO: 28,
|
|
8
|
+
REQUEST_ENTITY: 31,
|
|
9
|
+
RESPOND_ENTITY: 32,
|
|
10
|
+
END_RESPONSE: 35,
|
|
11
|
+
BROADCAST_TRANSACTION: 24,
|
|
12
|
+
REQUEST_SYSTEM_INFO: 46,
|
|
13
|
+
RESPOND_SYSTEM_INFO: 47,
|
|
14
|
+
REQUEST_ASSETS: 52,
|
|
15
|
+
RESPOND_ASSETS: 53,
|
|
16
|
+
} as const;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createPacketFramer } from "./packet-framer.js";
|
|
3
|
+
import { encodeRequestResponseHeader } from "./request-response-header.js";
|
|
4
|
+
|
|
5
|
+
function makePacket(type: number, dejavu: number, payload: Uint8Array): Uint8Array {
|
|
6
|
+
const header = encodeRequestResponseHeader({
|
|
7
|
+
size: 8 + payload.byteLength,
|
|
8
|
+
type,
|
|
9
|
+
dejavu,
|
|
10
|
+
});
|
|
11
|
+
const out = new Uint8Array(header.byteLength + payload.byteLength);
|
|
12
|
+
out.set(header, 0);
|
|
13
|
+
out.set(payload, 8);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("createPacketFramer", () => {
|
|
18
|
+
it("yields nothing with insufficient header bytes", () => {
|
|
19
|
+
const framer = createPacketFramer();
|
|
20
|
+
framer.push(Uint8Array.from([1, 2, 3]));
|
|
21
|
+
expect(framer.read()).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("parses a complete packet from a single chunk", () => {
|
|
25
|
+
const framer = createPacketFramer();
|
|
26
|
+
const payload = Uint8Array.from([9, 8, 7]);
|
|
27
|
+
framer.push(makePacket(24, 0, payload));
|
|
28
|
+
|
|
29
|
+
const packets = framer.read();
|
|
30
|
+
expect(packets.length).toBe(1);
|
|
31
|
+
const packet0 = packets[0];
|
|
32
|
+
if (!packet0) throw new Error("Expected first packet");
|
|
33
|
+
expect(packet0.header).toEqual({ size: 11, type: 24, dejavu: 0 });
|
|
34
|
+
expect([...packet0.payload]).toEqual([9, 8, 7]);
|
|
35
|
+
expect(framer.bufferedBytes()).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("parses multiple packets from a single chunk", () => {
|
|
39
|
+
const framer = createPacketFramer();
|
|
40
|
+
const a = makePacket(27, 1, new Uint8Array(0));
|
|
41
|
+
const b = makePacket(28, 2, Uint8Array.from([1]));
|
|
42
|
+
const combined = new Uint8Array(a.length + b.length);
|
|
43
|
+
combined.set(a, 0);
|
|
44
|
+
combined.set(b, a.length);
|
|
45
|
+
|
|
46
|
+
framer.push(combined);
|
|
47
|
+
|
|
48
|
+
const packets = framer.read();
|
|
49
|
+
expect(packets.length).toBe(2);
|
|
50
|
+
const packet0 = packets[0];
|
|
51
|
+
const packet1 = packets[1];
|
|
52
|
+
if (!packet0 || !packet1) throw new Error("Expected two packets");
|
|
53
|
+
expect(packet0.header.type).toBe(27);
|
|
54
|
+
expect(packet0.payload.byteLength).toBe(0);
|
|
55
|
+
expect(packet1.header.type).toBe(28);
|
|
56
|
+
expect([...packet1.payload]).toEqual([1]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("handles header split across chunks", () => {
|
|
60
|
+
const framer = createPacketFramer();
|
|
61
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
62
|
+
const packet = makePacket(31, 123, payload);
|
|
63
|
+
|
|
64
|
+
framer.push(packet.subarray(0, 2));
|
|
65
|
+
expect(framer.read()).toEqual([]);
|
|
66
|
+
|
|
67
|
+
framer.push(packet.subarray(2, 8));
|
|
68
|
+
expect(framer.read()).toEqual([]);
|
|
69
|
+
|
|
70
|
+
framer.push(packet.subarray(8));
|
|
71
|
+
const packets = framer.read();
|
|
72
|
+
expect(packets.length).toBe(1);
|
|
73
|
+
const packet0 = packets[0];
|
|
74
|
+
if (!packet0) throw new Error("Expected first packet");
|
|
75
|
+
expect(packet0.header).toEqual({ size: 12, type: 31, dejavu: 123 });
|
|
76
|
+
expect([...packet0.payload]).toEqual([1, 2, 3, 4]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("handles payload split across chunks", () => {
|
|
80
|
+
const framer = createPacketFramer();
|
|
81
|
+
const payload = Uint8Array.from([5, 6, 7, 8, 9]);
|
|
82
|
+
const packet = makePacket(16, 999, payload);
|
|
83
|
+
|
|
84
|
+
framer.push(packet.subarray(0, 10));
|
|
85
|
+
expect(framer.read()).toEqual([]);
|
|
86
|
+
|
|
87
|
+
framer.push(packet.subarray(10));
|
|
88
|
+
const packets = framer.read();
|
|
89
|
+
expect(packets.length).toBe(1);
|
|
90
|
+
const packet0 = packets[0];
|
|
91
|
+
if (!packet0) throw new Error("Expected first packet");
|
|
92
|
+
expect([...packet0.payload]).toEqual([5, 6, 7, 8, 9]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("throws on invalid packet size (<8)", () => {
|
|
96
|
+
const framer = createPacketFramer();
|
|
97
|
+
const badHeader = encodeRequestResponseHeader({ size: 1, type: 1, dejavu: 0 });
|
|
98
|
+
framer.push(badHeader);
|
|
99
|
+
expect(() => framer.read()).toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|