sdk/js-query: add EthCallByTimestamp mock support
This commit is contained in:
parent
43b34b18bd
commit
e64db08af5
|
@ -1,3 +1,7 @@
|
|||
## 0.0.7
|
||||
|
||||
Add EthCallByTimestamp mock support
|
||||
|
||||
## 0.0.6
|
||||
|
||||
Deserialization support
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue