sdk/js-query: add EthCallByTimestamp mock support

This commit is contained in:
Evan Gray 2024-01-11 17:44:04 -05:00 committed by Evan Gray
parent 43b34b18bd
commit e64db08af5
4 changed files with 256 additions and 1 deletions

View File

@ -1,3 +1,7 @@
## 0.0.7
Add EthCallByTimestamp mock support
## 0.0.6
Deserialization support

View File

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

View File

@ -2,6 +2,8 @@ import axios from "axios";
import { Buffer } from "buffer";
import {
ChainQueryType,
EthCallByTimestampQueryRequest,
EthCallByTimestampQueryResponse,
EthCallQueryRequest,
EthCallQueryResponse,
EthCallWithFinalityQueryRequest,
@ -202,6 +204,150 @@ export class QueryProxyMock {
)
)
);
} else if (type === ChainQueryType.EthCallByTimeStamp) {
const query = perChainRequest.query as EthCallByTimestampQueryRequest;
// Verify that the two block hints are consistent, either both set, or both unset.
if (
(query.targetBlockHint === "") !==
(query.followingBlockHint === "")
) {
throw new Error(
`Invalid block id hints in eth_call_by_timestamp query request, both should be either set or unset`
);
}
let targetBlock = query.targetBlockHint;
let followingBlock = query.followingBlockHint;
if (targetBlock === "") {
let nextQueryBlock = "latest";
let tries = 0;
let targetTimestamp = BigInt(0);
let followingTimestamp = BigInt(0);
while (
query.targetTimestamp < targetTimestamp ||
query.targetTimestamp >= followingTimestamp
) {
if (tries > 128) {
throw new Error(`Timestamp was not within the last 128 blocks.`);
}
// TODO: batching
const blockResult = (
await axios.post(rpc, {
jsonrpc: "2.0",
id: 1,
method: "eth_getBlockByNumber",
params: [nextQueryBlock, false],
})
).data?.result;
if (!blockResult) {
throw new Error(
`Invalid block result while searching for timestamp of ${nextQueryBlock}`
);
}
followingBlock = targetBlock;
followingTimestamp = targetTimestamp;
targetBlock = blockResult.number;
targetTimestamp =
BigInt(parseInt(blockResult.timestamp.substring(2), 16)) *
BigInt("1000000"); // time in seconds -> microseconds
nextQueryBlock = `0x${(
BigInt(blockResult.number) - BigInt(1)
).toString(16)}`;
tries++;
}
}
const response = await axios.post(rpc, [
...query.callData.map((args, idx) => ({
jsonrpc: "2.0",
id: idx,
method: "eth_call",
params: [
args,
//TODO: support block hash
targetBlock,
],
})),
{
jsonrpc: "2.0",
id: query.callData.length,
method: "eth_getBlockByNumber",
params: [targetBlock, false],
},
{
jsonrpc: "2.0",
id: query.callData.length,
method: "eth_getBlockByNumber",
params: [followingBlock, false],
},
]);
const callResults = response?.data?.slice(0, query.callData.length);
const targetBlockResult =
response?.data?.[query.callData.length]?.result;
const followingBlockResult =
response?.data?.[query.callData.length + 1]?.result;
if (
!targetBlockResult ||
!targetBlockResult.number ||
!targetBlockResult.timestamp ||
!targetBlockResult.hash
) {
throw new Error(
`Invalid target block result for chain ${perChainRequest.chainId} block ${query.targetBlockHint}`
);
}
if (
!followingBlockResult ||
!followingBlockResult.number ||
!followingBlockResult.timestamp ||
!followingBlockResult.hash
) {
throw new Error(
`Invalid following block result for chain ${perChainRequest.chainId} tag ${query.followingBlockHint}`
);
}
/*
target_block.timestamp <= target_time < following_block.timestamp
and
following_block_num - 1 == target_block_num
*/
const targetBlockNumber = BigInt(
parseInt(targetBlockResult.number.substring(2), 16)
);
const followingBlockNumber = BigInt(
parseInt(followingBlockResult.number.substring(2), 16)
);
if (targetBlockNumber + BigInt(1) !== followingBlockNumber) {
throw new Error(
`eth_call_by_timestamp query blocks are not adjacent`
);
}
const targetTimestamp =
BigInt(parseInt(targetBlockResult.timestamp.substring(2), 16)) *
BigInt("1000000"); // time in seconds -> microseconds
const followingTimestamp =
BigInt(parseInt(followingBlockResult.timestamp.substring(2), 16)) *
BigInt("1000000"); // time in seconds -> microseconds
if (
query.targetTimestamp < targetTimestamp ||
query.targetTimestamp >= followingTimestamp
) {
throw new Error(
`eth_call_by_timestamp desired timestamp falls outside of block range`
);
}
queryResponse.responses.push(
new PerChainQueryResponse(
perChainRequest.chainId,
new EthCallByTimestampQueryResponse(
BigInt(parseInt(targetBlockResult.number.substring(2), 16)), // block number
targetBlockResult.hash, // hash
targetTimestamp,
BigInt(parseInt(followingBlockResult.number.substring(2), 16)), // block number
followingBlockResult.hash, // hash
followingTimestamp,
callResults.map((callResult: any) => callResult.result)
)
)
);
} else {
throw new Error(`Unsupported query type for mock: ${type}`);
}

View File

@ -9,6 +9,7 @@ import {
import axios from "axios";
import { eth } from "web3";
import {
EthCallByTimestampQueryRequest,
EthCallQueryRequest,
EthCallWithFinalityQueryRequest,
PerChainQueryRequest,
@ -127,4 +128,108 @@ describe.skip("mocks match testnet", () => {
// const matchesReal = mock.sign(serializedResponse);
// expect(matchesReal).toEqual(realResponse.signatures);
});
test("EthCallByTimestampQueryRequest mock matches testnet", async () => {
const targetTimestamp =
BigInt(Date.now() - 1000 * 30) * // thirty seconds ago
BigInt(1000); // milliseconds to microseconds
const arbitrumDemoContract = "0x6E36177f26A3C9cD2CE8DDF1b12904fe36deA47F";
const data = eth.abi.encodeFunctionSignature("getMyCounter()");
const query = new QueryRequest(42, [
new PerChainQueryRequest(
23,
new EthCallByTimestampQueryRequest(targetTimestamp, "", "", [
{ to: arbitrumDemoContract, data },
])
),
]);
const { bytes } = await mock.mock(query);
// from CCQ Demo UI
const signatureNotRequiredApiKey = "2d6c22c6-afae-4e54-b36d-5ba118da646a";
const realResponse = (
await axios.post<QueryProxyQueryResponse>(
QUERY_URL,
{
bytes: Buffer.from(query.serialize()).toString("hex"),
},
{ headers: { "X-API-Key": signatureNotRequiredApiKey } }
)
).data;
// the mock has an empty request signature, whereas the real service is signed
// we'll empty out the sig to compare the bytes
const realResponseWithEmptySignature = `${realResponse.bytes.substring(
0,
6
)}${Buffer.from(new Array(65)).toString(
"hex"
)}${realResponse.bytes.substring(6 + 65 * 2)}`;
expect(bytes).toEqual(realResponseWithEmptySignature);
});
test("EthCallByTimestampQueryRequest fails with non-adjacent blocks", async () => {
expect.assertions(1);
const targetTimestamp =
BigInt(Date.now() - 1000 * 60 * 1) * // one minute ago
BigInt(1000); // milliseconds to microseconds
const blockNumber = (
await axios.post(ARBITRUM_NODE_URL, {
jsonrpc: "2.0",
id: 1,
method: "eth_getBlockByNumber",
params: ["finalized", false],
})
).data?.result?.number;
const arbitrumDemoContract = "0x6E36177f26A3C9cD2CE8DDF1b12904fe36deA47F";
const data = eth.abi.encodeFunctionSignature("getMyCounter()");
const query = new QueryRequest(42, [
new PerChainQueryRequest(
23,
new EthCallByTimestampQueryRequest(
targetTimestamp,
blockNumber,
blockNumber,
[{ to: arbitrumDemoContract, data }]
)
),
]);
try {
await mock.mock(query);
} catch (e: any) {
expect(e.message).toMatch(
"eth_call_by_timestamp query blocks are not adjacent"
);
}
});
test("EthCallByTimestampQueryRequest fails with wrong timestamp", async () => {
expect.assertions(1);
const targetTimestamp =
BigInt(Date.now() - 1000 * 60 * 30) * // thirty minutes ago
BigInt(1000); // milliseconds to microseconds
const blockNumber = (
await axios.post(ARBITRUM_NODE_URL, {
jsonrpc: "2.0",
id: 1,
method: "eth_getBlockByNumber",
params: ["finalized", false],
})
).data?.result?.number;
const arbitrumDemoContract = "0x6E36177f26A3C9cD2CE8DDF1b12904fe36deA47F";
const data = eth.abi.encodeFunctionSignature("getMyCounter()");
const query = new QueryRequest(42, [
new PerChainQueryRequest(
23,
new EthCallByTimestampQueryRequest(
targetTimestamp,
blockNumber,
`0x${(parseInt(blockNumber, 16) + 1).toString(16)}`,
[{ to: arbitrumDemoContract, data }]
)
),
]);
try {
await mock.mock(query);
} catch (e: any) {
expect(e.message).toMatch(
"eth_call_by_timestamp desired timestamp falls outside of block range"
);
}
});
});