sdk/js-query: deserialization support

This commit is contained in:
Evan Gray 2023-11-09 22:01:44 -05:00 committed by Evan Gray
parent 11bc1a5a91
commit 65701f95f5
13 changed files with 696 additions and 72 deletions

View File

@ -1,3 +1,7 @@
## 0.0.6
Deserialization support
## 0.0.5
Mock support

View File

@ -1,6 +1,6 @@
{
"name": "@wormhole-foundation/wormhole-query-sdk",
"version": "0.0.5",
"version": "0.0.6",
"description": "Wormhole cross-chain query SDK",
"homepage": "https://wormhole.com",
"main": "./lib/cjs/index.js",

View File

@ -3,21 +3,16 @@ import { Buffer } from "buffer";
import {
ChainQueryType,
EthCallQueryRequest,
EthCallQueryResponse,
EthCallWithFinalityQueryRequest,
EthCallWithFinalityQueryResponse,
PerChainQueryResponse,
QueryProxyQueryResponse,
QueryRequest,
hexToUint8Array,
QueryResponse,
sign,
} from "../query";
import { BinaryWriter } from "../query/BinaryWriter";
import { BytesLike } from "@ethersproject/bytes";
import { keccak256 } from "@ethersproject/keccak256";
export type QueryProxyQueryResponse = {
signatures: string[];
bytes: string;
};
const QUERY_RESPONSE_PREFIX = "query_response_0000000000000000000|";
/**
* Usage:
@ -51,15 +46,8 @@ export class QueryProxyMock {
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0",
]
) {}
sign(serializedResponse: BytesLike) {
const digest = hexToUint8Array(
keccak256(
Buffer.concat([
Buffer.from(QUERY_RESPONSE_PREFIX),
hexToUint8Array(keccak256(serializedResponse)),
])
)
);
sign(serializedResponse: Uint8Array) {
const digest = QueryResponse.digest(serializedResponse);
return this.mockPrivateKeys.map(
(key, idx) => `${sign(key, digest)}${idx.toString(16).padStart(2, "0")}`
);
@ -82,14 +70,11 @@ export class QueryProxyMock {
* @returns a promise result matching the query proxy's query response
*/
async mock(queryRequest: QueryRequest): Promise<QueryProxyQueryResponse> {
const serializedRequest = queryRequest.serialize();
const writer = new BinaryWriter()
.writeUint8(1) // version
.writeUint16(0) // source = off-chain
.writeUint8Array(new Uint8Array(new Array(65))) // empty signature for mock
.writeUint32(serializedRequest.length)
.writeUint8Array(serializedRequest)
.writeUint8(queryRequest.requests.length);
const queryResponse = new QueryResponse(
0, // source = off-chain
Buffer.from(new Array(65)).toString("hex"), // empty signature for mock
queryRequest
);
for (const perChainRequest of queryRequest.requests) {
const rpc = this.rpcMap[perChainRequest.chainId];
if (!rpc) {
@ -98,7 +83,6 @@ export class QueryProxyMock {
);
}
const type = perChainRequest.query.type();
writer.writeUint16(perChainRequest.chainId).writeUint8(type);
if (type === ChainQueryType.EthCall) {
const query = perChainRequest.query as EthCallQueryRequest;
const response = await axios.post(rpc, [
@ -132,25 +116,18 @@ export class QueryProxyMock {
`Invalid block result for chain ${perChainRequest.chainId} block ${query.blockTag}`
);
}
const results = callResults.map(
(callResult: any) =>
new Uint8Array(Buffer.from(callResult.result.substring(2), "hex"))
queryResponse.responses.push(
new PerChainQueryResponse(
perChainRequest.chainId,
new EthCallQueryResponse(
BigInt(parseInt(blockResult.number.substring(2), 16)), // block number
blockResult.hash, // hash
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000"), // time in seconds -> microseconds
callResults.map((callResult: any) => callResult.result)
)
)
);
const perChainWriter = new BinaryWriter()
.writeUint64(BigInt(parseInt(blockResult.number.substring(2), 16))) // block number
.writeUint8Array(
new Uint8Array(Buffer.from(blockResult.hash.substring(2), "hex"))
) // hash
.writeUint64(
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000")
) // time in seconds -> microseconds
.writeUint8(results.length);
for (const result of results) {
perChainWriter.writeUint32(result.length).writeUint8Array(result);
}
const serialized = perChainWriter.data();
writer.writeUint32(serialized.length).writeUint8Array(serialized);
} else if (type === ChainQueryType.EthCallWithFinality) {
const query = perChainRequest.query as EthCallWithFinalityQueryRequest;
const response = await axios.post(rpc, [
@ -213,30 +190,23 @@ export class QueryProxyMock {
`Requested block for eth_call_with_finality has not yet reached the requested finality. Block: ${blockNumber}, ${query.finality}: ${latestBlockNumberMatchingFinality}`
);
}
const results = callResults.map(
(callResult: any) =>
new Uint8Array(Buffer.from(callResult.result.substring(2), "hex"))
queryResponse.responses.push(
new PerChainQueryResponse(
perChainRequest.chainId,
new EthCallWithFinalityQueryResponse(
BigInt(parseInt(blockResult.number.substring(2), 16)), // block number
blockResult.hash, // hash
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000"), // time in seconds -> microseconds
callResults.map((callResult: any) => callResult.result)
)
)
);
const perChainWriter = new BinaryWriter()
.writeUint64(blockNumber) // block number
.writeUint8Array(
new Uint8Array(Buffer.from(blockResult.hash.substring(2), "hex"))
) // hash
.writeUint64(
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000")
) // time in seconds -> microseconds
.writeUint8(results.length);
for (const result of results) {
perChainWriter.writeUint32(result.length).writeUint8Array(result);
}
const serialized = perChainWriter.data();
writer.writeUint32(serialized.length).writeUint8Array(serialized);
} else {
throw new Error(`Unsupported query type for mock: ${type}`);
}
}
const serializedResponse = writer.data();
const serializedResponse = queryResponse.serialize();
return {
signatures: this.sign(serializedResponse),
bytes: Buffer.from(serializedResponse).toString("hex"),

View File

@ -0,0 +1,58 @@
import { Buffer } from "buffer";
import { uint8ArrayToHex } from "./utils";
// BinaryReader provides the inverse of BinaryWriter
// Numbers are encoded as big endian
export class BinaryReader {
private _buffer: Buffer;
private _offset: number;
constructor(
arrayBuffer: WithImplicitCoercion<ArrayBuffer | SharedArrayBuffer>
) {
this._buffer = Buffer.from(arrayBuffer);
this._offset = 0;
}
readUint8(): number {
const tmp = this._buffer.readUint8(this._offset);
this._offset += 1;
return tmp;
}
readUint16(): number {
const tmp = this._buffer.readUint16BE(this._offset);
this._offset += 2;
return tmp;
}
readUint32(): number {
const tmp = this._buffer.readUint32BE(this._offset);
this._offset += 4;
return tmp;
}
readUint64(): bigint {
const tmp = this._buffer.readBigUInt64BE(this._offset);
this._offset += 8;
return tmp;
}
readUint8Array(length: number): Uint8Array {
const tmp = this._buffer.subarray(this._offset, this._offset + length);
this._offset += length;
return new Uint8Array(tmp);
}
readHex(length: number): string {
return uint8ArrayToHex(this.readUint8Array(length));
}
readString(length: number): string {
const tmp = this._buffer
.subarray(this._offset, this._offset + length)
.toString();
this._offset += length;
return tmp;
}
}

View File

@ -2,7 +2,9 @@ import { Buffer } from "buffer";
import { BinaryWriter } from "./BinaryWriter";
import { HexString } from "./consts";
import { ChainQueryType, ChainSpecificQuery } from "./request";
import { hexToUint8Array, isValidHexString } from "./utils";
import { coalesceUint8Array, hexToUint8Array, isValidHexString } from "./utils";
import { BinaryReader } from "./BinaryReader";
import { ChainSpecificResponse } from "./response";
export interface EthCallData {
to: string;
@ -37,6 +39,25 @@ export class EthCallQueryRequest implements ChainSpecificQuery {
});
return writer.data();
}
static from(bytes: string | Uint8Array): EthCallQueryRequest {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): EthCallQueryRequest {
const blockTagLength = reader.readUint32();
const blockTag = reader.readString(blockTagLength);
const callDataLength = reader.readUint8();
const callData: EthCallData[] = [];
for (let idx = 0; idx < callDataLength; idx++) {
const to = reader.readHex(20);
const dataLength = reader.readUint32();
const data = reader.readHex(dataLength);
callData.push({ to, data });
}
return new EthCallQueryRequest(blockTag, callData);
}
}
export function parseBlockId(blockId: BlockTag): string {
@ -59,3 +80,48 @@ export function parseBlockId(blockId: BlockTag): string {
return blockId;
}
export class EthCallQueryResponse implements ChainSpecificResponse {
constructor(
public blockNumber: bigint,
public blockHash: string,
public blockTime: bigint,
public results: string[] = []
) {}
type(): ChainQueryType {
return ChainQueryType.EthCall;
}
serialize(): Uint8Array {
const writer = new BinaryWriter()
.writeUint64(this.blockNumber)
.writeUint8Array(hexToUint8Array(this.blockHash))
.writeUint64(this.blockTime)
.writeUint8(this.results.length);
for (const result of this.results) {
const arr = hexToUint8Array(result);
writer.writeUint32(arr.length).writeUint8Array(arr);
}
return writer.data();
}
static from(bytes: string | Uint8Array): EthCallQueryResponse {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): EthCallQueryResponse {
const blockNumber = reader.readUint64();
const blockHash = reader.readHex(32);
const blockTime = reader.readUint64();
const resultsLength = reader.readUint8();
const results: string[] = [];
for (let idx = 0; idx < resultsLength; idx++) {
const resultLength = reader.readUint32();
const result = reader.readHex(resultLength);
results.push(result);
}
return new EthCallQueryResponse(blockNumber, blockHash, blockTime, results);
}
}

View File

@ -2,7 +2,9 @@ import { Buffer } from "buffer";
import { BinaryWriter } from "./BinaryWriter";
import { BlockTag, EthCallData } from "./ethCall";
import { ChainQueryType, ChainSpecificQuery } from "./request";
import { hexToUint8Array, isValidHexString } from "./utils";
import { coalesceUint8Array, hexToUint8Array, isValidHexString } from "./utils";
import { BinaryReader } from "./BinaryReader";
import { ChainSpecificResponse } from "./response";
export class EthCallByTimestampQueryRequest implements ChainSpecificQuery {
targetTimestamp: bigint;
@ -41,6 +43,33 @@ export class EthCallByTimestampQueryRequest implements ChainSpecificQuery {
});
return writer.data();
}
static from(bytes: string | Uint8Array): EthCallByTimestampQueryRequest {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): EthCallByTimestampQueryRequest {
const targetTimestamp = reader.readUint64();
const targetBlockHintLength = reader.readUint32();
const targetBlockHint = reader.readString(targetBlockHintLength);
const followingBlockHintLength = reader.readUint32();
const followingBlockHint = reader.readString(followingBlockHintLength);
const callDataLength = reader.readUint8();
const callData: EthCallData[] = [];
for (let idx = 0; idx < callDataLength; idx++) {
const to = reader.readHex(20);
const dataLength = reader.readUint32();
const data = reader.readHex(dataLength);
callData.push({ to, data });
}
return new EthCallByTimestampQueryRequest(
targetTimestamp,
targetBlockHint,
followingBlockHint,
callData
);
}
}
function parseBlockHint(blockHint: BlockTag): string {
@ -62,3 +91,65 @@ function parseBlockHint(blockHint: BlockTag): string {
return blockHint;
}
export class EthCallByTimestampQueryResponse implements ChainSpecificResponse {
constructor(
public targetBlockNumber: bigint,
public targetBlockHash: string,
public targetBlockTime: bigint,
public followingBlockNumber: bigint,
public followingBlockHash: string,
public followingBlockTime: bigint,
public results: string[] = []
) {}
type(): ChainQueryType {
return ChainQueryType.EthCallByTimeStamp;
}
serialize(): Uint8Array {
const writer = new BinaryWriter()
.writeUint64(this.targetBlockNumber)
.writeUint8Array(hexToUint8Array(this.targetBlockHash))
.writeUint64(this.targetBlockTime)
.writeUint64(this.followingBlockNumber)
.writeUint8Array(hexToUint8Array(this.followingBlockHash))
.writeUint64(this.followingBlockTime)
.writeUint8(this.results.length);
for (const result of this.results) {
const arr = hexToUint8Array(result);
writer.writeUint32(arr.length).writeUint8Array(arr);
}
return writer.data();
}
static from(bytes: string | Uint8Array): EthCallByTimestampQueryResponse {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): EthCallByTimestampQueryResponse {
const targetBlockNumber = reader.readUint64();
const targetBlockHash = reader.readHex(32);
const targetBlockTime = reader.readUint64();
const followingBlockNumber = reader.readUint64();
const followingBlockHash = reader.readHex(32);
const followingBlockTime = reader.readUint64();
const resultsLength = reader.readUint8();
const results: string[] = [];
for (let idx = 0; idx < resultsLength; idx++) {
const resultLength = reader.readUint32();
const result = reader.readHex(resultLength);
results.push(result);
}
return new EthCallByTimestampQueryResponse(
targetBlockNumber,
targetBlockHash,
targetBlockTime,
followingBlockNumber,
followingBlockHash,
followingBlockTime,
results
);
}
}

View File

@ -1,7 +1,14 @@
import { BinaryReader } from "./BinaryReader";
import { BinaryWriter } from "./BinaryWriter";
import { BlockTag, EthCallData, parseBlockId } from "./ethCall";
import {
BlockTag,
EthCallData,
EthCallQueryResponse,
parseBlockId,
} from "./ethCall";
import { ChainQueryType, ChainSpecificQuery } from "./request";
import { hexToUint8Array, isValidHexString } from "./utils";
import { ChainSpecificResponse } from "./response";
import { coalesceUint8Array, hexToUint8Array, isValidHexString } from "./utils";
export class EthCallWithFinalityQueryRequest implements ChainSpecificQuery {
blockId: string;
@ -36,4 +43,49 @@ export class EthCallWithFinalityQueryRequest implements ChainSpecificQuery {
});
return writer.data();
}
static from(bytes: string | Uint8Array): EthCallWithFinalityQueryRequest {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): EthCallWithFinalityQueryRequest {
const blockTagLength = reader.readUint32();
const blockTag = reader.readString(blockTagLength);
const finalityLength = reader.readUint32();
const finality = reader.readString(finalityLength);
if (finality != "finalized" && finality != "safe") {
throw new Error(`Unsupported finality: ${finality}`);
}
const callDataLength = reader.readUint8();
const callData: EthCallData[] = [];
for (let idx = 0; idx < callDataLength; idx++) {
const to = reader.readHex(20);
const dataLength = reader.readUint32();
const data = reader.readHex(dataLength);
callData.push({ to, data });
}
return new EthCallWithFinalityQueryRequest(blockTag, finality, callData);
}
}
export class EthCallWithFinalityQueryResponse extends EthCallQueryResponse {
type(): ChainQueryType {
return ChainQueryType.EthCallWithFinality;
}
static from(bytes: string | Uint8Array): EthCallWithFinalityQueryResponse {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): EthCallWithFinalityQueryResponse {
const queryResponse = EthCallQueryResponse.fromReader(reader);
return new EthCallWithFinalityQueryResponse(
queryResponse.blockNumber,
queryResponse.blockHash,
queryResponse.blockTime,
queryResponse.results
);
}
}

View File

@ -1,4 +1,6 @@
export * from "./request";
export * from "./response";
export * from "./proxy";
export * from "./utils";
export * from "./ethCall";
export * from "./ethCallByTimestamp";

View File

@ -0,0 +1,4 @@
export type QueryProxyQueryResponse = {
signatures: string[];
bytes: string;
};

View File

@ -2,7 +2,11 @@ import { keccak256 } from "@ethersproject/keccak256";
import { Buffer } from "buffer";
import { BinaryWriter } from "./BinaryWriter";
import { Network } from "./consts";
import { hexToUint8Array } from "./utils";
import { coalesceUint8Array, hexToUint8Array } from "./utils";
import { BinaryReader } from "./BinaryReader";
import { EthCallQueryRequest } from "./ethCall";
import { EthCallByTimestampQueryRequest } from "./ethCallByTimestamp";
import { EthCallWithFinalityQueryRequest } from "./ethCallWithFinality";
export const MAINNET_QUERY_REQUEST_PREFIX =
"mainnet_query_request_000000000000|";
@ -21,11 +25,13 @@ export function getPrefix(network: Network) {
: DEVNET_QUERY_REQUEST_PREFIX;
}
const REQUEST_VERSION = 1;
export class QueryRequest {
constructor(
public nonce: number,
public requests: PerChainQueryRequest[] = [],
public version: number = 1
public version: number = REQUEST_VERSION
) {}
serialize(): Uint8Array {
@ -44,6 +50,25 @@ export class QueryRequest {
const data = Buffer.concat([Buffer.from(prefix), bytes]);
return hexToUint8Array(keccak256(data).slice(2));
}
static from(bytes: string | Uint8Array): QueryRequest {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): QueryRequest {
const version = reader.readUint8();
if (version != REQUEST_VERSION) {
throw new Error(`Unsupported message version: ${version}`);
}
const nonce = reader.readUint32();
const queryRequest = new QueryRequest(nonce);
const numPerChainQueries = reader.readUint8();
for (let idx = 0; idx < numPerChainQueries; idx++) {
queryRequest.requests.push(PerChainQueryRequest.fromReader(reader));
}
return queryRequest;
}
}
export class PerChainQueryRequest {
@ -59,6 +84,28 @@ export class PerChainQueryRequest {
.writeUint8Array(queryData)
.data();
}
static from(bytes: string | Uint8Array): PerChainQueryRequest {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): PerChainQueryRequest {
const chainId = reader.readUint16();
const queryType = reader.readUint8();
reader.readUint32(); // skip the query length
let query: ChainSpecificQuery;
if (queryType === ChainQueryType.EthCall) {
query = EthCallQueryRequest.fromReader(reader);
} else if (queryType === ChainQueryType.EthCallByTimeStamp) {
query = EthCallByTimestampQueryRequest.fromReader(reader);
} else if (queryType === ChainQueryType.EthCallWithFinality) {
query = EthCallWithFinalityQueryRequest.fromReader(reader);
} else {
throw new Error(`Unsupported query type: ${queryType}`);
}
return new PerChainQueryRequest(chainId, query);
}
}
export interface ChainSpecificQuery {

View File

@ -0,0 +1,203 @@
import { describe, expect, test } from "@jest/globals";
import {
ChainQueryType,
EthCallByTimestampQueryRequest,
EthCallByTimestampQueryResponse,
EthCallQueryRequest,
EthCallQueryResponse,
EthCallWithFinalityQueryRequest,
EthCallWithFinalityQueryResponse,
PerChainQueryRequest,
PerChainQueryResponse,
QueryRequest,
QueryResponse,
} from "..";
describe("from works with hex and Uint8Array", () => {
test("QueryResponse", () => {
const result =
"010000b094a2ee9b1d5b310e1710bb5f6106bd481f28d932f83d6220c8ffdd5c55b91818ca78c812cd03c51338e384ab09265aa6fb2615a830e13c65775e769c5505800100000037010000002a010005010000002a0000000930783238343236626201130db1b83d205562461ed0720b37f1fbc21bf67f00000004916d5743010005010000005500000000028426bb7e422fe7df070cd5261d8e23280debfd1ac8c544dcd80837c5f1ebda47c06b7f000609c35ffdb8800100000020000000000000000000000000000000000000000000000000000000000000002a";
const queryResponseFromHex = QueryResponse.from(result);
const queryResponseFromUint8Array = QueryResponse.from(
Buffer.from(result, "hex")
);
expect(queryResponseFromHex.serialize()).toEqual(
queryResponseFromUint8Array.serialize()
);
});
});
describe("from yields known results", () => {
test("demo contract call", () => {
const result =
"010000b094a2ee9b1d5b310e1710bb5f6106bd481f28d932f83d6220c8ffdd5c55b91818ca78c812cd03c51338e384ab09265aa6fb2615a830e13c65775e769c5505800100000037010000002a010005010000002a0000000930783238343236626201130db1b83d205562461ed0720b37f1fbc21bf67f00000004916d5743010005010000005500000000028426bb7e422fe7df070cd5261d8e23280debfd1ac8c544dcd80837c5f1ebda47c06b7f000609c35ffdb8800100000020000000000000000000000000000000000000000000000000000000000000002a";
const queryResponse = QueryResponse.from(Buffer.from(result, "hex"));
expect(queryResponse.version).toEqual(1);
expect(queryResponse.requestChainId).toEqual(0);
expect(queryResponse.requestId).toEqual(
"0xb094a2ee9b1d5b310e1710bb5f6106bd481f28d932f83d6220c8ffdd5c55b91818ca78c812cd03c51338e384ab09265aa6fb2615a830e13c65775e769c55058001"
);
expect(queryResponse.request.version).toEqual(1);
expect(queryResponse.request.nonce).toEqual(42);
expect(queryResponse.request.requests.length).toEqual(1);
expect(queryResponse.request.requests[0].chainId).toEqual(5);
expect(queryResponse.request.requests[0].query.type()).toEqual(
ChainQueryType.EthCall
);
expect(
(queryResponse.request.requests[0].query as EthCallQueryRequest).blockTag
).toEqual("0x28426bb");
expect(
(queryResponse.request.requests[0].query as EthCallQueryRequest).callData
.length
).toEqual(1);
expect(
(queryResponse.request.requests[0].query as EthCallQueryRequest)
.callData[0].to
).toEqual("0x130db1b83d205562461ed0720b37f1fbc21bf67f");
expect(
(queryResponse.request.requests[0].query as EthCallQueryRequest)
.callData[0].data
).toEqual("0x916d5743");
expect(queryResponse.responses.length).toEqual(1);
expect(queryResponse.responses[0].chainId).toEqual(5);
expect(queryResponse.responses[0].response.type()).toEqual(
ChainQueryType.EthCall
);
expect(
(queryResponse.responses[0].response as EthCallQueryResponse).blockNumber
).toEqual(BigInt(42215099));
expect(
(queryResponse.responses[0].response as EthCallQueryResponse).blockHash
).toEqual(
"0x7e422fe7df070cd5261d8e23280debfd1ac8c544dcd80837c5f1ebda47c06b7f"
);
expect(
(queryResponse.responses[0].response as EthCallQueryResponse).blockTime
).toEqual(BigInt(1699584594000000));
expect(
(queryResponse.responses[0].response as EthCallQueryResponse).results
.length
).toEqual(1);
expect(
(queryResponse.responses[0].response as EthCallQueryResponse).results[0]
).toEqual(
"0x000000000000000000000000000000000000000000000000000000000000002a"
);
});
});
describe("serialize and from are inverse", () => {
test("EthCallQueryResponse", () => {
const serializedResponse = new EthCallQueryResponse(
BigInt("12344321"),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt("789789789"),
["0xdeadbeef", "0x00", "0x01"]
).serialize();
expect(EthCallQueryResponse.from(serializedResponse).serialize()).toEqual(
serializedResponse
);
});
test("EthCallByTimestampQueryResponse", () => {
const serializedResponse = new EthCallByTimestampQueryResponse(
BigInt("12344321"),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt("789789789"),
BigInt("12344321"),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt("789789789"),
["0xdeadbeef", "0x00", "0x01"]
).serialize();
expect(
EthCallByTimestampQueryResponse.from(serializedResponse).serialize()
).toEqual(serializedResponse);
});
test("EthCallWithFinalityQueryResponse", () => {
const serializedResponse = new EthCallWithFinalityQueryResponse(
BigInt("12344321"),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt("789789789"),
["0xdeadbeef", "0x00", "0x01"]
).serialize();
expect(
EthCallWithFinalityQueryResponse.from(serializedResponse).serialize()
).toEqual(serializedResponse);
});
const exampleQueryRequest = new QueryRequest(42, [
new PerChainQueryRequest(
5,
new EthCallQueryRequest(987654, [
{
to: "0x130Db1B83d205562461eD0720B37f1FBC21Bf67F",
data: "0x01234567",
},
])
),
new PerChainQueryRequest(
2,
new EthCallByTimestampQueryRequest(BigInt(99999999), 12345, 45678, [
{
to: "0x130Db1B83d205562461eD0720B37f1FBC21Bf67F",
data: "0x01234567",
},
])
),
new PerChainQueryRequest(
23,
new EthCallWithFinalityQueryRequest(987654, "finalized", [
{
to: "0x130Db1B83d205562461eD0720B37f1FBC21Bf67F",
data: "0x01234567",
},
])
),
]);
test("QueryRequest", () => {
const serializedRequest = exampleQueryRequest.serialize();
expect(QueryRequest.from(serializedRequest).serialize()).toEqual(
serializedRequest
);
});
test("QueryResponse", () => {
const serializedResponse = new QueryResponse(
0,
Buffer.from(new Array(65)).toString("hex"),
exampleQueryRequest,
[
new PerChainQueryResponse(
5,
new EthCallQueryResponse(
BigInt(987654),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt(99998888),
["0xdeadbeef", "0x00", "0x01"]
)
),
new PerChainQueryResponse(
2,
new EthCallByTimestampQueryResponse(
BigInt(987654),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt(99998888),
BigInt(987654),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt(99998888),
["0xdeadbeef", "0x00", "0x01"]
)
),
new PerChainQueryResponse(
23,
new EthCallWithFinalityQueryResponse(
BigInt(987654),
"0x123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123a",
BigInt(99998888),
["0xdeadbeef", "0x00", "0x01"]
)
),
]
).serialize();
expect(QueryResponse.from(serializedResponse).serialize()).toEqual(
serializedResponse
);
});
});

View File

@ -0,0 +1,119 @@
import { keccak256 } from "@ethersproject/keccak256";
import { Buffer } from "buffer";
import { coalesceUint8Array, hexToUint8Array } from "./utils";
import { BinaryReader } from "./BinaryReader";
import { ChainQueryType, ChainSpecificQuery, QueryRequest } from "./request";
import { BinaryWriter } from "./BinaryWriter";
import { EthCallQueryResponse } from "./ethCall";
import { EthCallByTimestampQueryResponse } from "./ethCallByTimestamp";
import { EthCallWithFinalityQueryResponse } from "./ethCallWithFinality";
export const QUERY_RESPONSE_PREFIX = "query_response_0000000000000000000|";
const RESPONSE_VERSION = 1;
export class QueryResponse {
constructor(
public requestChainId: number = 0,
public requestId: string,
public request: QueryRequest,
public responses: PerChainQueryResponse[] = [],
public version: number = RESPONSE_VERSION
) {}
serialize(): Uint8Array {
const serializedRequest = this.request.serialize();
const writer = new BinaryWriter()
.writeUint8(this.version)
.writeUint16(this.requestChainId)
.writeUint8Array(hexToUint8Array(this.requestId)) // TODO: this only works for hex encoded signatures
.writeUint32(serializedRequest.length)
.writeUint8Array(serializedRequest)
.writeUint8(this.responses.length);
for (const response of this.responses) {
writer.writeUint8Array(response.serialize());
}
return writer.data();
}
static digest(bytes: Uint8Array): Uint8Array {
return hexToUint8Array(
keccak256(
Buffer.concat([
Buffer.from(QUERY_RESPONSE_PREFIX),
hexToUint8Array(keccak256(bytes)),
])
)
);
}
static from(bytes: string | Uint8Array): QueryResponse {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): QueryResponse {
const version = reader.readUint8();
if (version != RESPONSE_VERSION) {
throw new Error(`Unsupported message version: ${version}`);
}
const requestChainId = reader.readUint16();
if (requestChainId !== 0) {
// TODO: support reading off-chain and on-chain requests
throw new Error(`Unsupported request chain: ${requestChainId}`);
}
const requestId = reader.readHex(65); // signature
reader.readUint32(); // skip the query length
const queryRequest = QueryRequest.fromReader(reader);
const queryResponse = new QueryResponse(
requestChainId,
requestId,
queryRequest
);
const numPerChainResponses = reader.readUint8();
for (let idx = 0; idx < numPerChainResponses; idx++) {
queryResponse.responses.push(PerChainQueryResponse.fromReader(reader));
}
return queryResponse;
}
}
export class PerChainQueryResponse {
constructor(public chainId: number, public response: ChainSpecificResponse) {}
serialize(): Uint8Array {
const writer = new BinaryWriter()
.writeUint16(this.chainId)
.writeUint8(this.response.type());
const queryResponse = this.response.serialize();
return writer
.writeUint32(queryResponse.length)
.writeUint8Array(queryResponse)
.data();
}
static from(bytes: string | Uint8Array): PerChainQueryResponse {
const reader = new BinaryReader(coalesceUint8Array(bytes));
return this.fromReader(reader);
}
static fromReader(reader: BinaryReader): PerChainQueryResponse {
const chainId = reader.readUint16();
const queryType = reader.readUint8();
reader.readUint32(); // skip the query length
let response: ChainSpecificResponse;
if (queryType === ChainQueryType.EthCall) {
response = EthCallQueryResponse.fromReader(reader);
} else if (queryType === ChainQueryType.EthCallByTimeStamp) {
response = EthCallByTimestampQueryResponse.fromReader(reader);
} else if (queryType === ChainQueryType.EthCallWithFinality) {
response = EthCallWithFinalityQueryResponse.fromReader(reader);
} else {
throw new Error(`Unsupported response type: ${queryType}`);
}
return new PerChainQueryResponse(chainId, response);
}
}
export interface ChainSpecificResponse extends ChainSpecificQuery {}

View File

@ -16,6 +16,14 @@ export function hexToUint8Array(s: string): Uint8Array {
return new Uint8Array(Buffer.from(s, "hex"));
}
export function uint8ArrayToHex(b: Uint8Array) {
return `0x${Buffer.from(b).toString("hex")}`;
}
export function coalesceUint8Array(b: string | Uint8Array): Uint8Array {
return typeof b === "string" ? hexToUint8Array(b) : b;
}
/**
* @param key Private key used to sign `data`
* @param data Data for signing