Skip to content

Commit f2cddc9

Browse files
committedMar 5, 2023
Setup jest
Add tests for pi message fetching Fix pi fetching bugs Add Unknown Message.status
1 parent 37c97b6 commit f2cddc9

9 files changed

+2176
-132
lines changed
 

‎.eslintignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ dist
33
build
44
coverage
55
next.config.js
6-
tailwind.config.js
6+
tailwind.config.js
7+
jest.config.js

‎jest.config.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const nextJest = require('next/jest')
2+
3+
const createJestConfig = nextJest({
4+
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
5+
dir: './',
6+
})
7+
8+
// Add any custom config to be passed to Jest
9+
/** @type {import('jest').Config} */
10+
const customJestConfig = {
11+
// Add more setup options before each test is run
12+
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
13+
}
14+
15+
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
16+
module.exports = createJestConfig(customJestConfig)

‎package.json

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"devDependencies": {
2828
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
29+
"@types/jest": "^29.4.0",
2930
"@types/node": "^18.11.18",
3031
"@types/react": "^18.0.27",
3132
"@types/react-dom": "^18.0.10",
@@ -35,6 +36,7 @@
3536
"eslint": "^8.34.0",
3637
"eslint-config-next": "^13.2.0",
3738
"eslint-config-prettier": "^8.6.0",
39+
"jest": "^29.4.3",
3840
"postcss": "^8.4.21",
3941
"prettier": "^2.8.4",
4042
"tailwindcss": "^3.2.7",
@@ -57,6 +59,7 @@
5759
"typecheck": "tsc",
5860
"lint": "next lint",
5961
"start": "next start",
62+
"test": "jest",
6063
"prettier": "prettier --write ./src"
6164
},
6265
"types": "dist/src/index.d.ts",

‎src/features/messages/cards/ContentDetailsCard.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SelectField } from '../../../components/input/SelectField';
88
import { Card } from '../../../components/layout/Card';
99
import { MAILBOX_VERSION } from '../../../consts/environments';
1010
import { Message } from '../../../types';
11+
import { ensureLeading0x } from '../../../utils/addresses';
1112
import { tryUtf8DecodeBytes } from '../../../utils/string';
1213

1314
import { CodeBlock, LabelAndCodeBlock } from './CodeBlock';
@@ -73,7 +74,7 @@ export function ContentDetailsCard({
7374
<KeyValueRow
7475
label="Message Id:"
7576
labelWidth="w-20"
76-
display={msgId}
77+
display={ensureLeading0x(msgId)}
7778
displayWidth="w-60 sm:w-80"
7879
showCopy={true}
7980
blurValue={shouldBlur}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { chainMetadata, hyperlaneCoreAddresses } from '@hyperlane-xyz/sdk';
2+
3+
import { ChainConfig } from '../../chains/chainConfig';
4+
5+
import { fetchMessagesFromPiChain } from './usePiChainMessageQuery';
6+
7+
jest.setTimeout(15000);
8+
9+
const goerliMailbox = hyperlaneCoreAddresses.goerli.mailbox;
10+
const goerliConfigWithExplorer: ChainConfig = {
11+
...chainMetadata.goerli,
12+
contracts: { mailbox: goerliMailbox },
13+
};
14+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
15+
const { blockExplorers, ...goerliConfigNoExplorer } = goerliConfigWithExplorer;
16+
17+
// https://explorer.hyperlane.xyz/message/0d9dc662da32d3737835295e9c9eb2b92ac630aec2756b93a187b8fd22a82afd
18+
const txHash = '0xb81ba87ee7ae30dea0f67f7f25b67d973cec6533e7407ea7a8c761f39d8dee1b';
19+
const msgId = '0x0d9dc662da32d3737835295e9c9eb2b92ac630aec2756b93a187b8fd22a82afd';
20+
const senderAddress = '0x0637a1360ea44602dae5c4ba515c2bcb6c762fbc';
21+
const recipientAddress = '0x921d3a71386d3ab8f3ad4ec91ce1556d5fc26859';
22+
23+
const goerliMessage = {
24+
body: '0x48656c6c6f21',
25+
destinationChainId: 44787,
26+
destinationDomainId: 44787,
27+
destinationTimestamp: 0,
28+
destinationTransaction: {
29+
blockNumber: 0,
30+
from: '0x0000000000000000000000000000000000000000',
31+
gasUsed: 0,
32+
timestamp: 0,
33+
transactionHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
34+
},
35+
id: '',
36+
msgId: '0x0d9dc662da32d3737835295e9c9eb2b92ac630aec2756b93a187b8fd22a82afd',
37+
nonce: 20763,
38+
originChainId: 5,
39+
originDomainId: 5,
40+
originTimestamp: 0,
41+
originTransaction: {
42+
blockNumber: 8600958,
43+
from: '0x0000000000000000000000000000000000000000',
44+
gasUsed: 0,
45+
timestamp: 0,
46+
transactionHash: '0xb81ba87ee7ae30dea0f67f7f25b67d973cec6533e7407ea7a8c761f39d8dee1b',
47+
},
48+
recipient: '0x000000000000000000000000921d3a71386d3ab8f3ad4ec91ce1556d5fc26859',
49+
sender: '0x0000000000000000000000000637a1360ea44602dae5c4ba515c2bcb6c762fbc',
50+
status: 'unknown',
51+
};
52+
53+
describe('fetchMessagesFromPiChain', () => {
54+
it('Fetches messages using explorer for tx hash', async () => {
55+
const messages = await fetchMessagesFromPiChain(goerliConfigWithExplorer, txHash);
56+
expect(messages).toEqual([goerliMessage]);
57+
});
58+
it.skip('Fetches messages using explorer for msg id', async () => {
59+
const messages = await fetchMessagesFromPiChain(goerliConfigWithExplorer, msgId);
60+
expect(messages).toEqual([goerliMessage]);
61+
});
62+
it.skip('Fetches messages using explorer for sender address', async () => {
63+
const messages = await fetchMessagesFromPiChain(goerliConfigWithExplorer, senderAddress);
64+
expect(messages).toEqual([goerliMessage]);
65+
});
66+
it.skip('Fetches messages using explorer for recipient address', async () => {
67+
const messages = await fetchMessagesFromPiChain(goerliConfigWithExplorer, recipientAddress);
68+
expect(messages).toEqual([goerliMessage]);
69+
});
70+
it('Fetches messages using provider for tx hash', async () => {
71+
const messages = await fetchMessagesFromPiChain(goerliConfigNoExplorer, txHash);
72+
expect(messages).toEqual([goerliMessage]);
73+
});
74+
it('Fetches messages using provider for msg id', async () => {
75+
const messages = await fetchMessagesFromPiChain(goerliConfigNoExplorer, msgId);
76+
expect(messages).toEqual([goerliMessage]);
77+
});
78+
it('Fetches messages using provider for sender address', async () => {
79+
const messages = await fetchMessagesFromPiChain(goerliConfigNoExplorer, senderAddress);
80+
const testMsg = messages.find((m) => m.msgId === msgId);
81+
expect([testMsg]).toEqual([goerliMessage]);
82+
});
83+
it('Fetches messages using provider for recipient address', async () => {
84+
const messages = await fetchMessagesFromPiChain(goerliConfigNoExplorer, recipientAddress);
85+
const testMsg = messages.find((m) => m.msgId === msgId);
86+
expect([testMsg]).toEqual([goerliMessage]);
87+
});
88+
it('Throws error for invalid input', async () => {
89+
await expect(
90+
fetchMessagesFromPiChain(goerliConfigWithExplorer, 'invalidInput'),
91+
).rejects.toThrow();
92+
});
93+
});

‎src/features/messages/queries/usePiChainMessageQuery.ts

+84-59
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useQuery } from '@tanstack/react-query';
2-
import { providers } from 'ethers';
2+
import { BigNumber, constants, ethers, providers } from 'ethers';
33

44
import { Mailbox__factory } from '@hyperlane-xyz/core';
55
import { utils } from '@hyperlane-xyz/utils';
66

77
import { getMultiProvider, getProvider } from '../../../multiProvider';
88
import { useStore } from '../../../store';
9-
import { Message, MessageStatus } from '../../../types';
9+
import { Message, MessageStatus, PartialTransactionReceipt } from '../../../types';
1010
import {
1111
ensureLeading0x,
1212
isValidAddressFast,
@@ -22,11 +22,13 @@ import { ChainConfig } from '../../chains/chainConfig';
2222

2323
import { isValidSearchQuery } from './useMessageQuery';
2424

25+
const PROVIDER_LOGS_BLOCK_WINDOW = 150_000;
26+
2527
const mailbox = Mailbox__factory.createInterface();
2628
const dispatchTopic0 = mailbox.getEventTopic('Dispatch');
2729
const dispatchIdTopic0 = mailbox.getEventTopic('DispatchId');
28-
const processTopic0 = mailbox.getEventTopic('Process');
29-
const processIdTopic0 = mailbox.getEventTopic('ProcessId');
30+
// const processTopic0 = mailbox.getEventTopic('Process');
31+
// const processIdTopic0 = mailbox.getEventTopic('ProcessId');
3032

3133
// Query 'Permissionless Interoperability (PI)' chains using custom
3234
// chain configs in store state
@@ -85,50 +87,50 @@ searchForMessages(input):
8587
GOTO hash search above
8688
*/
8789

88-
async function fetchMessagesFromPiChain(
90+
export async function fetchMessagesFromPiChain(
8991
chainConfig: ChainConfig,
9092
input: string,
9193
): Promise<Message[]> {
9294
const { chainId, blockExplorers } = chainConfig;
9395
const useExplorer = !!blockExplorers?.[0]?.apiUrl;
96+
const formattedInput = ensureLeading0x(input);
9497

95-
let logs: providers.Log[] | null = null;
96-
if (isValidAddressFast(input)) {
97-
logs = await fetchLogsForAddress(chainConfig, input, useExplorer);
98+
let logs: providers.Log[];
99+
if (isValidAddressFast(formattedInput)) {
100+
logs = await fetchLogsForAddress(chainConfig, formattedInput, useExplorer);
98101
} else if (isValidTransactionHash(input)) {
99-
logs = await fetchLogsForTxHash(chainConfig, input, useExplorer);
100-
if (!logs) {
102+
logs = await fetchLogsForTxHash(chainConfig, formattedInput, useExplorer);
103+
if (!logs.length) {
101104
// Input may be a msg id
102-
logs = await fetchLogsForMsgId(chainConfig, input, useExplorer);
105+
logs = await fetchLogsForMsgId(chainConfig, formattedInput, useExplorer);
103106
}
104107
} else {
105108
throw new Error('Invalid PI search input');
106109
}
107110

108-
if (!logs?.length) {
111+
if (!logs.length) {
109112
// Throw so Promise.any caller doesn't trigger
110113
throw new Error(`No messages found for chain ${chainId}`);
111114
}
112115

113-
return logs.map(logToMessage);
116+
return logs.map(logToMessage).filter((m): m is Message => !!m);
114117
}
115118

116119
async function fetchLogsForAddress(
117120
{ chainId, contracts }: ChainConfig,
118121
address: Address,
119122
useExplorer?: boolean,
120123
) {
124+
logger.debug(`Fetching logs for address ${address} on chain ${chainId}`);
121125
const mailboxAddr = contracts.mailbox;
122-
const dispatchTopic1 = ensureLeading0x(address);
123-
const dispatchTopic3 = utils.addressToBytes32(dispatchTopic1);
124-
const processTopic1 = dispatchTopic3;
125-
const processTopic3 = dispatchTopic1;
126+
const dispatchTopic1 = utils.addressToBytes32(address);
127+
const dispatchTopic3 = dispatchTopic1;
126128

127129
if (useExplorer) {
128130
return fetchLogsFromExplorer(
129131
[
130132
`&topic0=${dispatchTopic0}&topic0_1_opr=and&topic1=${dispatchTopic1}&topic1_3_opr=or&topic3=${dispatchTopic3}`,
131-
`&topic0=${processTopic0}&topic0_1_opr=and&topic1=${processTopic1}&topic1_3_opr=or&topic3=${processTopic3}`,
133+
// `&topic0=${processTopic0}&topic0_1_opr=and&topic1=${dispatchTopic3}&topic1_3_opr=or&topic3=${dispatchTopic1}`,
132134
],
133135
mailboxAddr,
134136
chainId,
@@ -138,8 +140,8 @@ async function fetchLogsForAddress(
138140
[
139141
[dispatchTopic0, dispatchTopic1],
140142
[dispatchTopic0, null, null, dispatchTopic3],
141-
[processTopic0, processTopic1],
142-
[processTopic0, null, null, processTopic3],
143+
// [processTopic0, dispatchTopic3],
144+
// [processTopic0, null, null, dispatchTopic1],
143145
],
144146
mailboxAddr,
145147
chainId,
@@ -148,55 +150,56 @@ async function fetchLogsForAddress(
148150
}
149151

150152
async function fetchLogsForTxHash({ chainId }: ChainConfig, txHash: string, useExplorer: boolean) {
153+
logger.debug(`Fetching logs for txHash ${txHash} on chain ${chainId}`);
151154
if (useExplorer) {
152155
try {
153-
const txReceipt = await queryExplorerForTxReceipt(chainId, txHash);
156+
const txReceipt = await queryExplorerForTxReceipt(chainId, txHash, false);
154157
logger.debug(`Tx receipt found from explorer for chain ${chainId}`);
155-
console.log(txReceipt);
156158
return txReceipt.logs;
157159
} catch (error) {
158160
logger.debug(`Tx hash not found in explorer for chain ${chainId}`);
159-
return null;
160161
}
161162
} else {
162163
const provider = getProvider(chainId);
163164
const txReceipt = await provider.getTransactionReceipt(txHash);
164-
console.log(txReceipt);
165165
if (txReceipt) {
166166
logger.debug(`Tx receipt found from provider for chain ${chainId}`);
167167
return txReceipt.logs;
168168
} else {
169169
logger.debug(`Tx hash not found from provider for chain ${chainId}`);
170-
return null;
171170
}
172171
}
172+
return [];
173173
}
174174

175175
async function fetchLogsForMsgId(chainConfig: ChainConfig, msgId: string, useExplorer: boolean) {
176176
const { contracts, chainId } = chainConfig;
177+
logger.debug(`Fetching logs for msgId ${msgId} on chain ${chainId}`);
177178
const mailboxAddr = contracts.mailbox;
178-
const topic1 = ensureLeading0x(msgId);
179+
const topic1 = msgId;
179180
let logs: providers.Log[];
180181
if (useExplorer) {
181182
logs = await fetchLogsFromExplorer(
182183
[
183184
`&topic0=${dispatchIdTopic0}&topic0_1_opr=and&topic1=${topic1}`,
184-
`&topic0=${processIdTopic0}&topic0_1_opr=and&topic1=${topic1}`,
185+
// `&topic0=${processIdTopic0}&topic0_1_opr=and&topic1=${topic1}`,
185186
],
186187
mailboxAddr,
187188
chainId,
188189
);
189190
} else {
190191
logs = await fetchLogsFromProvider(
191192
[
192-
[dispatchTopic0, topic1],
193-
[processTopic0, topic1],
193+
[dispatchIdTopic0, topic1],
194+
// [processIdTopic0, topic1],
194195
],
195196
mailboxAddr,
196197
chainId,
197198
);
198199
}
199200

201+
// Grab first tx hash found in any log and get all logs for that tx
202+
// Necessary because DispatchId/ProcessId logs don't contain useful info
200203
if (logs.length) {
201204
const txHash = logs[0].transactionHash;
202205
logger.debug('Found tx hash with log of msg id', txHash);
@@ -207,13 +210,13 @@ async function fetchLogsForMsgId(chainConfig: ChainConfig, msgId: string, useExp
207210
}
208211

209212
async function fetchLogsFromExplorer(paths: Array<string>, contractAddr: Address, chainId: number) {
210-
const pathBase = `api?module=logs&action=getLogs&fromBlock=0&toBlock=999999999&address=${contractAddr}`;
211-
const logs = (
212-
await Promise.all(paths.map((p) => queryExplorerForLogs(chainId, `${pathBase}${p}`)))
213-
)
214-
.flat()
215-
.map(toProviderLog);
216-
console.log(logs);
213+
const base = `?module=logs&action=getLogs&fromBlock=0&toBlock=999999999&address=${contractAddr}`;
214+
let logs: providers.Log[] = [];
215+
for (const path of paths) {
216+
// Originally use parallel requests here with Promise.all but immediately hit rate limit errors
217+
const result = await queryExplorerForLogs(chainId, `${base}${path}`, undefined, false);
218+
logs = [...logs, ...result.map(toProviderLog)];
219+
}
217220
return logs;
218221
}
219222

@@ -223,46 +226,68 @@ async function fetchLogsFromProvider(
223226
chainId: number,
224227
) {
225228
const provider = getProvider(chainId);
229+
const latestBlock = await provider.getBlockNumber();
226230
// TODO may need chunking here to avoid RPC errors
227231
const logs = (
228232
await Promise.all(
229233
topics.map((t) =>
230234
provider.getLogs({
235+
fromBlock: latestBlock - PROVIDER_LOGS_BLOCK_WINDOW,
236+
toBlock: 'latest',
231237
address: contractAddr,
232238
topics: t,
233239
}),
234240
),
235241
)
236242
).flat();
237-
console.log(logs);
238243
return logs;
239244
}
240245

241-
function logToMessage(log: providers.Log): Message {
246+
function logToMessage(log: providers.Log): Message | null {
247+
let logDesc: ethers.utils.LogDescription;
248+
try {
249+
logDesc = mailbox.parseLog(log);
250+
if (logDesc.name.toLowerCase() !== 'dispatch') return null;
251+
} catch (error) {
252+
// Probably not a message log, ignore
253+
return null;
254+
}
255+
256+
const bytes = logDesc.args['message'];
257+
const message = utils.parseMessage(bytes);
258+
259+
const tx: PartialTransactionReceipt = {
260+
from: constants.AddressZero, //TODO
261+
transactionHash: log.transactionHash,
262+
blockNumber: BigNumber.from(log.blockNumber).toNumber(),
263+
gasUsed: 0, //TODO
264+
timestamp: 0, // TODO
265+
};
266+
const emptyTx = {
267+
from: constants.AddressZero, //TODO
268+
transactionHash: constants.HashZero,
269+
blockNumber: 0,
270+
gasUsed: 0,
271+
timestamp: 0,
272+
};
273+
242274
const multiProvider = getMultiProvider();
243-
const bytes = mailbox.parseLog(log).args['message'];
244-
const parsed = utils.parseMessage(bytes);
275+
245276
return {
246277
id: '', // No db id exists
247278
msgId: utils.messageId(bytes),
248-
status: MessageStatus.Pending, // TODO
249-
sender: parsed.sender,
250-
recipient: parsed.recipient,
251-
originDomainId: parsed.origin,
252-
destinationDomainId: parsed.destination,
253-
originChainId: multiProvider.getChainId(parsed.origin),
254-
destinationChainId: multiProvider.getChainId(parsed.destination),
255-
originTimestamp: 0, // TODO
256-
destinationTimestamp: undefined, // TODO
257-
nonce: parsed.nonce,
258-
body: parsed.body,
259-
originTransaction: {
260-
from: '0x', //TODO
261-
transactionHash: log.transactionHash,
262-
blockNumber: log.blockNumber,
263-
gasUsed: 0,
264-
timestamp: 0, //TODO
265-
},
266-
destinationTransaction: undefined, // TODO
279+
status: MessageStatus.Unknown, // TODO
280+
sender: message.sender,
281+
recipient: message.recipient,
282+
originDomainId: message.origin,
283+
destinationDomainId: message.destination,
284+
originChainId: multiProvider.getChainId(message.origin),
285+
destinationChainId: multiProvider.getChainId(message.destination),
286+
originTimestamp: tx.timestamp, // TODO
287+
destinationTimestamp: 0, // TODO
288+
nonce: message.nonce,
289+
body: message.body,
290+
originTransaction: tx,
291+
destinationTransaction: emptyTx,
267292
};
268293
}

‎src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface PartialTransactionReceipt {
99

1010
// TODO consider reconciling with SDK's MessageStatus
1111
export enum MessageStatus {
12+
Unknown = 'unknown',
1213
Pending = 'pending',
1314
Delivered = 'delivered',
1415
Failing = 'failing',

‎src/utils/explorers.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { config } from '../consts/config';
44
import { getMultiProvider } from '../multiProvider';
55

66
import { logger } from './logger';
7-
import { retryAsync } from './retry';
87
import { fetchWithTimeout } from './timeout';
98
import { isValidHttpUrl } from './url';
109

@@ -27,9 +26,10 @@ export function getExplorerApiUrl(chainId: number) {
2726
}
2827

2928
export function getTxExplorerUrl(chainId: number, hash?: string) {
30-
const baseUrl = getExplorerUrl(chainId);
31-
if (!hash || !baseUrl) return null;
32-
return `${baseUrl}/tx/${hash}`;
29+
if (!hash) return null;
30+
const url = getMultiProvider().getExplorerTxUrl(chainId, { hash });
31+
if (isValidHttpUrl(url)) return url;
32+
else return null;
3333
}
3434

3535
export async function queryExplorer<P>(chainId: number, path: string, useKey = true) {
@@ -45,7 +45,7 @@ export async function queryExplorer<P>(chainId: number, path: string, useKey = t
4545
url += `&apikey=${apiKey}`;
4646
}
4747

48-
const result = await retryAsync(() => executeQuery<P>(url), 2, 1000);
48+
const result = await executeQuery<P>(url);
4949
return result;
5050
}
5151

@@ -114,7 +114,7 @@ export function toProviderLog(log: ExplorerLogEntry): providers.Log {
114114
}
115115

116116
export async function queryExplorerForTx(chainId: number, txHash: string, useKey = true) {
117-
const path = `api?module=proxy&action=eth_getTransactionByHash&txhash=${txHash}`;
117+
const path = `?module=proxy&action=eth_getTransactionByHash&txhash=${txHash}`;
118118
const tx = await queryExplorer<providers.TransactionResponse>(chainId, path, useKey);
119119
if (!tx || tx.hash.toLowerCase() !== txHash.toLowerCase()) {
120120
const msg = 'Invalid tx result';
@@ -125,7 +125,7 @@ export async function queryExplorerForTx(chainId: number, txHash: string, useKey
125125
}
126126

127127
export async function queryExplorerForTxReceipt(chainId: number, txHash: string, useKey = true) {
128-
const path = `api?module=proxy&action=eth_getTransactionReceipt&txhash=${txHash}`;
128+
const path = `?module=proxy&action=eth_getTransactionReceipt&txhash=${txHash}`;
129129
const tx = await queryExplorer<providers.TransactionReceipt>(chainId, path, useKey);
130130
if (!tx || tx.transactionHash.toLowerCase() !== txHash.toLowerCase()) {
131131
const msg = 'Invalid tx result';
@@ -140,7 +140,7 @@ export async function queryExplorerForBlock(
140140
blockNumber?: number | string,
141141
useKey = true,
142142
) {
143-
const path = `api?module=proxy&action=eth_getBlockByNumber&tag=${
143+
const path = `?module=proxy&action=eth_getBlockByNumber&tag=${
144144
blockNumber || 'latest'
145145
}&boolean=false`;
146146
const block = await queryExplorer<providers.Block>(chainId, path, useKey);

‎yarn.lock

+1,967-63
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.