sdk/js-query: deserialization support
This commit is contained in:
parent
11bc1a5a91
commit
65701f95f5
|
@ -1,3 +1,7 @@
|
|||
## 0.0.6
|
||||
|
||||
Deserialization support
|
||||
|
||||
## 0.0.5
|
||||
|
||||
Mock support
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export * from "./request";
|
||||
export * from "./response";
|
||||
export * from "./proxy";
|
||||
export * from "./utils";
|
||||
export * from "./ethCall";
|
||||
export * from "./ethCallByTimestamp";
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export type QueryProxyQueryResponse = {
|
||||
signatures: string[];
|
||||
bytes: string;
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 {}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue