@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,127 @@
|
|
|
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 NUMBER_OF_TRANSACTIONS_PER_TICK = 1024;
|
|
6
|
+
export const MAX_NUMBER_OF_CONTRACTS = 1024;
|
|
7
|
+
export const SIGNATURE_LENGTH = 64;
|
|
8
|
+
|
|
9
|
+
export const TICK_DATA_PAYLOAD_SIZE =
|
|
10
|
+
16 + // fixed header fields
|
|
11
|
+
32 + // timelock
|
|
12
|
+
NUMBER_OF_TRANSACTIONS_PER_TICK * 32 +
|
|
13
|
+
MAX_NUMBER_OF_CONTRACTS * 8 +
|
|
14
|
+
SIGNATURE_LENGTH;
|
|
15
|
+
|
|
16
|
+
export type TickDataView = Readonly<{
|
|
17
|
+
computorIndex: number; // uint16
|
|
18
|
+
epoch: number; // uint16
|
|
19
|
+
tick: number; // uint32
|
|
20
|
+
|
|
21
|
+
millisecond: number; // uint16
|
|
22
|
+
second: number; // uint8
|
|
23
|
+
minute: number; // uint8
|
|
24
|
+
hour: number; // uint8
|
|
25
|
+
day: number; // uint8
|
|
26
|
+
month: number; // uint8
|
|
27
|
+
year: number; // uint8
|
|
28
|
+
|
|
29
|
+
timelock32: Uint8Array;
|
|
30
|
+
signature64: Uint8Array;
|
|
31
|
+
|
|
32
|
+
getTransactionDigest(index: number): Uint8Array;
|
|
33
|
+
getContractFee(index: number): bigint;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
function assertU32(value: number, name: string) {
|
|
37
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
38
|
+
throw new RangeError(`${name} must be a uint32`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function encodeRequestTickData(tick: number): Uint8Array {
|
|
43
|
+
assertU32(tick, "tick");
|
|
44
|
+
const payload = new Uint8Array(4);
|
|
45
|
+
new DataView(payload.buffer).setUint32(0, tick, true);
|
|
46
|
+
return encodeRequestPacket(NetworkMessageType.REQUEST_TICK_DATA, payload);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function decodeBroadcastFutureTickData(payload: Uint8Array): TickDataView {
|
|
50
|
+
if (!(payload instanceof Uint8Array)) {
|
|
51
|
+
throw new TypeError("payload must be a Uint8Array");
|
|
52
|
+
}
|
|
53
|
+
if (payload.byteLength !== TICK_DATA_PAYLOAD_SIZE) {
|
|
54
|
+
throw new RangeError(`payload must be ${TICK_DATA_PAYLOAD_SIZE} bytes`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
58
|
+
|
|
59
|
+
const computorIndex = view.getUint16(0, true);
|
|
60
|
+
const epoch = view.getUint16(2, true);
|
|
61
|
+
const tick = view.getUint32(4, true);
|
|
62
|
+
|
|
63
|
+
const millisecond = view.getUint16(8, true);
|
|
64
|
+
const second = view.getUint8(10);
|
|
65
|
+
const minute = view.getUint8(11);
|
|
66
|
+
const hour = view.getUint8(12);
|
|
67
|
+
const day = view.getUint8(13);
|
|
68
|
+
const month = view.getUint8(14);
|
|
69
|
+
const year = view.getUint8(15);
|
|
70
|
+
|
|
71
|
+
const timelockOffset = 16;
|
|
72
|
+
const transactionDigestsOffset = timelockOffset + 32;
|
|
73
|
+
const contractFeesOffset = transactionDigestsOffset + NUMBER_OF_TRANSACTIONS_PER_TICK * 32;
|
|
74
|
+
const signatureOffset = contractFeesOffset + MAX_NUMBER_OF_CONTRACTS * 8;
|
|
75
|
+
|
|
76
|
+
const timelock32 = payload.slice(timelockOffset, timelockOffset + 32);
|
|
77
|
+
const signature64 = payload.slice(signatureOffset, signatureOffset + SIGNATURE_LENGTH);
|
|
78
|
+
|
|
79
|
+
const getTransactionDigest = (index: number): Uint8Array => {
|
|
80
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= NUMBER_OF_TRANSACTIONS_PER_TICK) {
|
|
81
|
+
throw new RangeError("index out of range");
|
|
82
|
+
}
|
|
83
|
+
const start = transactionDigestsOffset + index * 32;
|
|
84
|
+
return payload.subarray(start, start + 32);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getContractFee = (index: number): bigint => {
|
|
88
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= MAX_NUMBER_OF_CONTRACTS) {
|
|
89
|
+
throw new RangeError("index out of range");
|
|
90
|
+
}
|
|
91
|
+
const start = contractFeesOffset + index * 8;
|
|
92
|
+
return readI64LE(payload, start);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
computorIndex,
|
|
97
|
+
epoch,
|
|
98
|
+
tick,
|
|
99
|
+
millisecond,
|
|
100
|
+
second,
|
|
101
|
+
minute,
|
|
102
|
+
hour,
|
|
103
|
+
day,
|
|
104
|
+
month,
|
|
105
|
+
year,
|
|
106
|
+
timelock32,
|
|
107
|
+
signature64,
|
|
108
|
+
getTransactionDigest,
|
|
109
|
+
getContractFee,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function countNonZeroTransactionDigests(tickData: TickDataView): number {
|
|
114
|
+
let count = 0;
|
|
115
|
+
for (let i = 0; i < NUMBER_OF_TRANSACTIONS_PER_TICK; i++) {
|
|
116
|
+
const d = tickData.getTransactionDigest(i);
|
|
117
|
+
let nonZero = false;
|
|
118
|
+
for (let j = 0; j < d.byteLength; j++) {
|
|
119
|
+
if ((d[j] ?? 0) !== 0) {
|
|
120
|
+
nonZero = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (nonZero) count++;
|
|
125
|
+
}
|
|
126
|
+
return count;
|
|
127
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
3
|
+
import { decodeRequestResponseHeader } from "./request-response-header.js";
|
|
4
|
+
import { decodeRespondCurrentTickInfo, encodeRequestCurrentTickInfo } from "./tick-info.js";
|
|
5
|
+
|
|
6
|
+
describe("tick-info", () => {
|
|
7
|
+
it("encodes request current tick info packet", () => {
|
|
8
|
+
const packet = encodeRequestCurrentTickInfo();
|
|
9
|
+
expect(packet.byteLength).toBe(8);
|
|
10
|
+
const header = decodeRequestResponseHeader(packet);
|
|
11
|
+
expect(header.size).toBe(8);
|
|
12
|
+
expect(header.type).toBe(NetworkMessageType.REQUEST_CURRENT_TICK_INFO);
|
|
13
|
+
expect(header.dejavu).not.toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("decodes respond current tick info payload", () => {
|
|
17
|
+
const payload = new Uint8Array(16);
|
|
18
|
+
const view = new DataView(payload.buffer);
|
|
19
|
+
view.setUint16(0, 3000, true);
|
|
20
|
+
view.setUint16(2, 42, true);
|
|
21
|
+
view.setUint32(4, 123_456, true);
|
|
22
|
+
view.setUint16(8, 10, true);
|
|
23
|
+
view.setUint16(10, 2, true);
|
|
24
|
+
view.setUint32(12, 1_000, true);
|
|
25
|
+
|
|
26
|
+
expect(decodeRespondCurrentTickInfo(payload)).toEqual({
|
|
27
|
+
tickDuration: 3000,
|
|
28
|
+
epoch: 42,
|
|
29
|
+
tick: 123_456,
|
|
30
|
+
numberOfAlignedVotes: 10,
|
|
31
|
+
numberOfMisalignedVotes: 2,
|
|
32
|
+
initialTick: 1_000,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NetworkMessageType } from "./message-types.js";
|
|
2
|
+
import { encodeRequestPacket } from "./request-packet.js";
|
|
3
|
+
|
|
4
|
+
export type CurrentTickInfo = Readonly<{
|
|
5
|
+
tickDuration: number; // uint16
|
|
6
|
+
epoch: number; // uint16
|
|
7
|
+
tick: number; // uint32
|
|
8
|
+
numberOfAlignedVotes: number; // uint16
|
|
9
|
+
numberOfMisalignedVotes: number; // uint16
|
|
10
|
+
initialTick: number; // uint32
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
export function encodeRequestCurrentTickInfo(): Uint8Array {
|
|
14
|
+
return encodeRequestPacket(NetworkMessageType.REQUEST_CURRENT_TICK_INFO);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function decodeRespondCurrentTickInfo(payload: Uint8Array): CurrentTickInfo {
|
|
18
|
+
if (!(payload instanceof Uint8Array)) {
|
|
19
|
+
throw new TypeError("payload must be a Uint8Array");
|
|
20
|
+
}
|
|
21
|
+
if (payload.byteLength !== 16) {
|
|
22
|
+
throw new RangeError("payload must be 16 bytes");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
26
|
+
return {
|
|
27
|
+
tickDuration: view.getUint16(0, true),
|
|
28
|
+
epoch: view.getUint16(2, true),
|
|
29
|
+
tick: view.getUint32(4, true),
|
|
30
|
+
numberOfAlignedVotes: view.getUint16(8, true),
|
|
31
|
+
numberOfMisalignedVotes: view.getUint16(10, true),
|
|
32
|
+
initialTick: view.getUint32(12, true),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { verify } from "../crypto/schnorrq.js";
|
|
3
|
+
import { privateKeyFromSeed } from "../crypto/seed.js";
|
|
4
|
+
import { publicKeyFromIdentity } from "../primitives/identity.js";
|
|
5
|
+
import {
|
|
6
|
+
buildSignedTransaction,
|
|
7
|
+
buildUnsignedTransaction,
|
|
8
|
+
SIGNATURE_LENGTH,
|
|
9
|
+
TRANSACTION_HEADER_SIZE,
|
|
10
|
+
transactionId,
|
|
11
|
+
unsignedTransactionDigest,
|
|
12
|
+
} from "./transaction.js";
|
|
13
|
+
|
|
14
|
+
describe("transaction", () => {
|
|
15
|
+
it("encodes the transaction header correctly", () => {
|
|
16
|
+
const sourcePublicKey32 = new Uint8Array(32).fill(1);
|
|
17
|
+
const destinationPublicKey32 = new Uint8Array(32).fill(2);
|
|
18
|
+
const amount = 123n;
|
|
19
|
+
const tick = 456;
|
|
20
|
+
|
|
21
|
+
const unsigned = buildUnsignedTransaction({
|
|
22
|
+
sourcePublicKey32,
|
|
23
|
+
destinationPublicKey32,
|
|
24
|
+
amount,
|
|
25
|
+
tick,
|
|
26
|
+
inputType: 0,
|
|
27
|
+
inputBytes: new Uint8Array(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(unsigned.byteLength).toBe(TRANSACTION_HEADER_SIZE);
|
|
31
|
+
expect(unsigned.subarray(0, 32)).toEqual(sourcePublicKey32);
|
|
32
|
+
expect(unsigned.subarray(32, 64)).toEqual(destinationPublicKey32);
|
|
33
|
+
|
|
34
|
+
const view = new DataView(unsigned.buffer, unsigned.byteOffset, unsigned.byteLength);
|
|
35
|
+
expect(view.getBigInt64(64, true)).toBe(amount);
|
|
36
|
+
expect(view.getUint32(72, true)).toBe(tick);
|
|
37
|
+
expect(view.getUint16(76, true)).toBe(0);
|
|
38
|
+
expect(view.getUint16(78, true)).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("signs and verifies a simple transfer transaction", async () => {
|
|
42
|
+
const seed = "jvhbyzjinlyutyuhsweuxiwootqoevjqwqmdhjeohrytxjxidpbcfyg";
|
|
43
|
+
const destId = "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ";
|
|
44
|
+
|
|
45
|
+
const secretKey32 = await privateKeyFromSeed(seed);
|
|
46
|
+
const destinationPublicKey32 = publicKeyFromIdentity(destId);
|
|
47
|
+
const sourcePublicKey32 = publicKeyFromIdentity(
|
|
48
|
+
"HZEBBDSKZRTAWGYMTTSDZQDXYWPBUKBEAIYZNFLVWARZJBEBIJRRFKUDVETA",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const unsigned = buildUnsignedTransaction({
|
|
52
|
+
sourcePublicKey32,
|
|
53
|
+
destinationPublicKey32,
|
|
54
|
+
amount: 1n,
|
|
55
|
+
tick: 12345,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const digest32 = await unsignedTransactionDigest(unsigned);
|
|
59
|
+
expect(digest32.byteLength).toBe(32);
|
|
60
|
+
|
|
61
|
+
const signed = await buildSignedTransaction(
|
|
62
|
+
{
|
|
63
|
+
sourcePublicKey32,
|
|
64
|
+
destinationPublicKey32,
|
|
65
|
+
amount: 1n,
|
|
66
|
+
tick: 12345,
|
|
67
|
+
},
|
|
68
|
+
secretKey32,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(signed.byteLength).toBe(TRANSACTION_HEADER_SIZE + SIGNATURE_LENGTH);
|
|
72
|
+
|
|
73
|
+
const signature64 = signed.subarray(signed.byteLength - SIGNATURE_LENGTH);
|
|
74
|
+
expect(verify(sourcePublicKey32, digest32, signature64)).toBe(1);
|
|
75
|
+
|
|
76
|
+
const id = await transactionId(signed);
|
|
77
|
+
expect(id.length).toBe(60);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { k12 } from "../crypto/k12.js";
|
|
2
|
+
import { sign } from "../crypto/schnorrq.js";
|
|
3
|
+
import { identityFromPublicKey } from "../primitives/identity.js";
|
|
4
|
+
import { MAX_I64, MIN_I64, writeI64LE } from "../primitives/number64.js";
|
|
5
|
+
|
|
6
|
+
export const TRANSACTION_HEADER_SIZE = 80;
|
|
7
|
+
export const SIGNATURE_LENGTH = 64;
|
|
8
|
+
export const MAX_TRANSACTION_SIZE = 1024;
|
|
9
|
+
export const MAX_INPUT_SIZE = MAX_TRANSACTION_SIZE - (TRANSACTION_HEADER_SIZE + SIGNATURE_LENGTH);
|
|
10
|
+
|
|
11
|
+
export type BuildUnsignedTransactionParams = Readonly<{
|
|
12
|
+
sourcePublicKey32: Uint8Array;
|
|
13
|
+
destinationPublicKey32: Uint8Array;
|
|
14
|
+
amount: bigint | number;
|
|
15
|
+
tick: number;
|
|
16
|
+
inputType?: number;
|
|
17
|
+
inputBytes?: Uint8Array;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
function assertUint8ArrayLength(bytes: Uint8Array, length: number, name: string) {
|
|
21
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
22
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
23
|
+
}
|
|
24
|
+
if (bytes.byteLength !== length) {
|
|
25
|
+
throw new RangeError(`${name} must be ${length} bytes`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertU32(value: number, name: string) {
|
|
30
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
31
|
+
throw new RangeError(`${name} must be a uint32`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertU16(value: number, name: string) {
|
|
36
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff) {
|
|
37
|
+
throw new RangeError(`${name} must be a uint16`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toI64(value: bigint | number, name: string): bigint {
|
|
42
|
+
const bigintValue = typeof value === "number" ? BigInt(value) : value;
|
|
43
|
+
if (typeof value === "number") {
|
|
44
|
+
if (!Number.isFinite(value) || !Number.isSafeInteger(value)) {
|
|
45
|
+
throw new RangeError(`${name} must be a safe integer`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (bigintValue < MIN_I64 || bigintValue > MAX_I64) {
|
|
49
|
+
throw new RangeError(`${name} must fit in int64`);
|
|
50
|
+
}
|
|
51
|
+
return bigintValue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildUnsignedTransaction(params: BuildUnsignedTransactionParams): Uint8Array {
|
|
55
|
+
const inputType = params.inputType ?? 0;
|
|
56
|
+
const inputBytes = params.inputBytes ?? new Uint8Array();
|
|
57
|
+
|
|
58
|
+
assertUint8ArrayLength(params.sourcePublicKey32, 32, "sourcePublicKey32");
|
|
59
|
+
assertUint8ArrayLength(params.destinationPublicKey32, 32, "destinationPublicKey32");
|
|
60
|
+
assertU16(inputType, "inputType");
|
|
61
|
+
assertU32(params.tick, "tick");
|
|
62
|
+
|
|
63
|
+
if (!(inputBytes instanceof Uint8Array)) {
|
|
64
|
+
throw new TypeError("inputBytes must be a Uint8Array");
|
|
65
|
+
}
|
|
66
|
+
if (inputBytes.byteLength > MAX_INPUT_SIZE) {
|
|
67
|
+
throw new RangeError(`inputBytes must be <= ${MAX_INPUT_SIZE} bytes`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const inputSize = inputBytes.byteLength;
|
|
71
|
+
const out = new Uint8Array(TRANSACTION_HEADER_SIZE + inputSize);
|
|
72
|
+
out.set(params.sourcePublicKey32, 0);
|
|
73
|
+
out.set(params.destinationPublicKey32, 32);
|
|
74
|
+
writeI64LE(toI64(params.amount, "amount"), out, 64);
|
|
75
|
+
|
|
76
|
+
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
77
|
+
view.setUint32(72, params.tick, true);
|
|
78
|
+
view.setUint16(76, inputType, true);
|
|
79
|
+
view.setUint16(78, inputSize, true);
|
|
80
|
+
out.set(inputBytes, TRANSACTION_HEADER_SIZE);
|
|
81
|
+
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function unsignedTransactionDigest(unsignedTxBytes: Uint8Array): Promise<Uint8Array> {
|
|
86
|
+
if (!(unsignedTxBytes instanceof Uint8Array)) {
|
|
87
|
+
throw new TypeError("unsignedTxBytes must be a Uint8Array");
|
|
88
|
+
}
|
|
89
|
+
return k12(unsignedTxBytes, 32);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function signTransaction(
|
|
93
|
+
unsignedTxBytes: Uint8Array,
|
|
94
|
+
secretKey32: Uint8Array,
|
|
95
|
+
): Promise<Uint8Array> {
|
|
96
|
+
if (!(unsignedTxBytes instanceof Uint8Array)) {
|
|
97
|
+
throw new TypeError("unsignedTxBytes must be a Uint8Array");
|
|
98
|
+
}
|
|
99
|
+
assertUint8ArrayLength(secretKey32, 32, "secretKey32");
|
|
100
|
+
if (unsignedTxBytes.byteLength < TRANSACTION_HEADER_SIZE) {
|
|
101
|
+
throw new RangeError("unsignedTxBytes is too short");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const view = new DataView(
|
|
105
|
+
unsignedTxBytes.buffer,
|
|
106
|
+
unsignedTxBytes.byteOffset,
|
|
107
|
+
unsignedTxBytes.byteLength,
|
|
108
|
+
);
|
|
109
|
+
const inputSize = view.getUint16(78, true);
|
|
110
|
+
if (unsignedTxBytes.byteLength !== TRANSACTION_HEADER_SIZE + inputSize) {
|
|
111
|
+
throw new RangeError("unsignedTxBytes length does not match inputSize");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const digest32 = await unsignedTransactionDigest(unsignedTxBytes);
|
|
115
|
+
const publicKey32 = unsignedTxBytes.subarray(0, 32);
|
|
116
|
+
return sign(secretKey32, publicKey32, digest32);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function buildSignedTransaction(
|
|
120
|
+
params: BuildUnsignedTransactionParams,
|
|
121
|
+
secretKey32: Uint8Array,
|
|
122
|
+
): Promise<Uint8Array> {
|
|
123
|
+
const unsignedTxBytes = buildUnsignedTransaction(params);
|
|
124
|
+
const signature64 = await signTransaction(unsignedTxBytes, secretKey32);
|
|
125
|
+
assertUint8ArrayLength(signature64, SIGNATURE_LENGTH, "signature64");
|
|
126
|
+
|
|
127
|
+
const out = new Uint8Array(unsignedTxBytes.byteLength + SIGNATURE_LENGTH);
|
|
128
|
+
out.set(unsignedTxBytes, 0);
|
|
129
|
+
out.set(signature64, unsignedTxBytes.byteLength);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function transactionDigest(txBytes: Uint8Array): Promise<Uint8Array> {
|
|
134
|
+
if (!(txBytes instanceof Uint8Array)) {
|
|
135
|
+
throw new TypeError("txBytes must be a Uint8Array");
|
|
136
|
+
}
|
|
137
|
+
if (txBytes.byteLength < TRANSACTION_HEADER_SIZE + SIGNATURE_LENGTH) {
|
|
138
|
+
throw new RangeError("txBytes is too short");
|
|
139
|
+
}
|
|
140
|
+
if (txBytes.byteLength > MAX_TRANSACTION_SIZE) {
|
|
141
|
+
throw new RangeError(`txBytes must be <= ${MAX_TRANSACTION_SIZE} bytes`);
|
|
142
|
+
}
|
|
143
|
+
return k12(txBytes, 32);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function transactionId(txBytes: Uint8Array): Promise<string> {
|
|
147
|
+
const digest32 = await transactionDigest(txBytes);
|
|
148
|
+
return identityFromPublicKey(digest32, { lowerCase: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export class AsyncQueue<T> implements AsyncIterable<T> {
|
|
2
|
+
private readonly values: T[] = [];
|
|
3
|
+
private readonly waiters: Array<(result: IteratorResult<T>) => void> = [];
|
|
4
|
+
private closed = false;
|
|
5
|
+
private failure: unknown = undefined;
|
|
6
|
+
|
|
7
|
+
push(value: T) {
|
|
8
|
+
if (this.closed) return;
|
|
9
|
+
|
|
10
|
+
const waiter = this.waiters.shift();
|
|
11
|
+
if (waiter) {
|
|
12
|
+
waiter({ value, done: false });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.values.push(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close() {
|
|
20
|
+
if (this.closed) return;
|
|
21
|
+
this.closed = true;
|
|
22
|
+
while (this.waiters.length) {
|
|
23
|
+
const waiter = this.waiters.shift();
|
|
24
|
+
waiter?.({ value: undefined as never, done: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
error(err: unknown) {
|
|
29
|
+
if (this.closed) return;
|
|
30
|
+
this.failure = err;
|
|
31
|
+
this.closed = true;
|
|
32
|
+
while (this.waiters.length) {
|
|
33
|
+
const waiter = this.waiters.shift();
|
|
34
|
+
waiter?.({ value: undefined as never, done: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
39
|
+
return {
|
|
40
|
+
next: () => {
|
|
41
|
+
if (this.failure !== undefined) {
|
|
42
|
+
return Promise.reject(this.failure);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const value = this.values.shift();
|
|
46
|
+
if (value !== undefined) {
|
|
47
|
+
return Promise.resolve({ value, done: false });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.closed) {
|
|
51
|
+
return Promise.resolve({ value: undefined as never, done: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Promise<IteratorResult<T>>((resolve) => {
|
|
55
|
+
this.waiters.push(resolve);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { BridgeWebSocketConstructor } from "./bridge.js";
|
|
3
|
+
import { createBridgeTransport } from "./bridge.js";
|
|
4
|
+
|
|
5
|
+
type Listener = (event: unknown) => void;
|
|
6
|
+
|
|
7
|
+
class FakeWebSocket {
|
|
8
|
+
static readonly CONNECTING = 0;
|
|
9
|
+
static readonly OPEN = 1;
|
|
10
|
+
static readonly CLOSING = 2;
|
|
11
|
+
static readonly CLOSED = 3;
|
|
12
|
+
|
|
13
|
+
readonly CONNECTING = FakeWebSocket.CONNECTING;
|
|
14
|
+
readonly OPEN = FakeWebSocket.OPEN;
|
|
15
|
+
readonly CLOSING = FakeWebSocket.CLOSING;
|
|
16
|
+
readonly CLOSED = FakeWebSocket.CLOSED;
|
|
17
|
+
|
|
18
|
+
readyState = FakeWebSocket.CONNECTING;
|
|
19
|
+
binaryType = "arraybuffer";
|
|
20
|
+
readonly sent: Uint8Array[] = [];
|
|
21
|
+
readonly listeners = new Map<string, Listener[]>();
|
|
22
|
+
|
|
23
|
+
constructor(public readonly url: string) {
|
|
24
|
+
queueMicrotask(() => this.open());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addEventListener(type: string, listener: Listener, options?: { once?: boolean }) {
|
|
28
|
+
const wrapper: Listener = options?.once
|
|
29
|
+
? (e) => {
|
|
30
|
+
this.removeEventListener(type, wrapper);
|
|
31
|
+
listener(e);
|
|
32
|
+
}
|
|
33
|
+
: listener;
|
|
34
|
+
const list = this.listeners.get(type) ?? [];
|
|
35
|
+
list.push(wrapper);
|
|
36
|
+
this.listeners.set(type, list);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
removeEventListener(type: string, listener: Listener) {
|
|
40
|
+
const list = this.listeners.get(type) ?? [];
|
|
41
|
+
this.listeners.set(
|
|
42
|
+
type,
|
|
43
|
+
list.filter((l) => l !== listener),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private emit(type: string, event: unknown) {
|
|
48
|
+
const list = this.listeners.get(type) ?? [];
|
|
49
|
+
for (const l of list) l(event);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
open() {
|
|
53
|
+
this.readyState = FakeWebSocket.OPEN;
|
|
54
|
+
this.emit("open", {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
message(data: Uint8Array | ArrayBuffer) {
|
|
58
|
+
this.emit("message", { data });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
send(data: Uint8Array) {
|
|
62
|
+
this.sent.push(new Uint8Array(data));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
close() {
|
|
66
|
+
this.readyState = FakeWebSocket.CLOSED;
|
|
67
|
+
this.emit("close", {});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("bridge transport", () => {
|
|
72
|
+
it("writes websocket binary messages", async () => {
|
|
73
|
+
let instance: FakeWebSocket | undefined;
|
|
74
|
+
const WebSocketImpl = class extends FakeWebSocket {
|
|
75
|
+
constructor(url: string) {
|
|
76
|
+
super(url);
|
|
77
|
+
instance = this;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const transport = await createBridgeTransport({
|
|
82
|
+
url: "ws://example",
|
|
83
|
+
WebSocketImpl: WebSocketImpl as unknown as BridgeWebSocketConstructor,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await transport.write(Uint8Array.from([1, 2, 3]));
|
|
87
|
+
expect(instance?.sent).toEqual([Uint8Array.from([1, 2, 3])]);
|
|
88
|
+
|
|
89
|
+
await transport.close();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("yields inbound messages", async () => {
|
|
93
|
+
let instance: FakeWebSocket | undefined;
|
|
94
|
+
const WebSocketImpl = class extends FakeWebSocket {
|
|
95
|
+
constructor(url: string) {
|
|
96
|
+
super(url);
|
|
97
|
+
instance = this;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const transport = await createBridgeTransport({
|
|
102
|
+
url: "ws://example",
|
|
103
|
+
WebSocketImpl: WebSocketImpl as unknown as BridgeWebSocketConstructor,
|
|
104
|
+
});
|
|
105
|
+
const iter = transport.read()[Symbol.asyncIterator]();
|
|
106
|
+
|
|
107
|
+
instance?.message(Uint8Array.from([9, 8, 7]));
|
|
108
|
+
const msg = await iter.next();
|
|
109
|
+
expect(msg.done).toBe(false);
|
|
110
|
+
expect(msg.value).toEqual(Uint8Array.from([9, 8, 7]));
|
|
111
|
+
|
|
112
|
+
await transport.close();
|
|
113
|
+
});
|
|
114
|
+
});
|