Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sdk/js-query: add EthCallByTimestamp mock support #3708

Merged
merged 1 commit into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/js-query/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.0.7

Add EthCallByTimestamp mock support

## 0.0.6

Deserialization support
Expand Down
2 changes: 1 addition & 1 deletion sdk/js-query/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
146 changes: 146 additions & 0 deletions sdk/js-query/src/mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import axios from "axios";
import { Buffer } from "buffer";
import {
ChainQueryType,
EthCallByTimestampQueryRequest,
EthCallByTimestampQueryResponse,
EthCallQueryRequest,
EthCallQueryResponse,
EthCallWithFinalityQueryRequest,
Expand Down Expand Up @@ -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}`);
}
Expand Down
105 changes: 105 additions & 0 deletions sdk/js-query/src/mock/mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import axios from "axios";
import { eth } from "web3";
import {
EthCallByTimestampQueryRequest,
EthCallQueryRequest,
EthCallWithFinalityQueryRequest,
PerChainQueryRequest,
Expand Down Expand Up @@ -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"
);
}
});
});