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

feat: basic delivery checking and warp route recipient tooling for Sealevel chains #209

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
20 changes: 17 additions & 3 deletions docs/guides/message-debugging.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

import CosmosMessageDelivered from "@site/src/components/CosmosMessageDelivered";
import NonEvmMessageDelivered from "@site/src/components/NonEvmMessageDelivered";
import SealevelWarpTransferInfo from "@site/src/components/SealevelWarpTransferInfo";

# Message debugging

Expand Down Expand Up @@ -36,18 +37,28 @@ import CosmosMessageDelivered from "@site/src/components/CosmosMessageDelivered"
<figcaption>Example on Mintscan showing the log on an origin transaction.</figcaption>
</figure>
</TabItem>
<TabItem value="sealevel" label="Sealevel Origin">
When viewing the origin transaction on the origin chain's block explorer, scroll to the logs at the very bottom.

The log starting with **Dispatched message to** contains the message ID.

<figure>
![](/img/message-debugging/dispatch-id-log-sealevel.png)
<figcaption>Example on Eclipse's explorer showing the log on an origin transaction.</figcaption>
</figure>
</TabItem>
</Tabs>
</div>
</details>

<details>
<summary>**Debugging a message to a Cosmos chain**</summary>
<summary>**Debugging a message to a non-EVM chain (e.g. a Cosmos or Sealevel chain)**</summary>
<div>
At the moment, the [Hyperlane Explorer](https://explorer.hyperlane.xyz) doesn't support non-EVM chains.

To check if your message has been delivered, select the destination chain and input your message ID below:

<CosmosMessageDelivered />
<NonEvmMessageDelivered chains={['neutron', 'injective', 'eclipsemainnet', 'solanamainnet']} />
</div>
</details>

Expand Down Expand Up @@ -110,6 +121,9 @@ import CosmosMessageDelivered from "@site/src/components/CosmosMessageDelivered"
<figcaption>Example on Mintscan showing the log on an origin transaction.</figcaption>
</figure>
</TabItem>
<TabItem value="sealevel" label="Sealevel Origin">
<SealevelWarpTransferInfo chains={['eclipsemainnet', 'solanamainnet']} />
</TabItem>
</Tabs>
</div>
</details>
Expand Down
1 change: 1 addition & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const config = {
},

plugins: [
'./webpackPlugin',
[
"@docusaurus/plugin-client-redirects",
{
Expand Down
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,25 @@
"@hyperlane-xyz/core": "5.3.0",
"@hyperlane-xyz/registry": "4.4.1",
"@hyperlane-xyz/sdk": "5.3.0",
"@hyperlane-xyz/utils": "^5.3.0",
"@mdx-js/react": "^3.0.0",
"assert": "^2.1.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"clsx": "^2.0.0",
"crypto-browserify": "^3.12.0",
"https-browserify": "^1.0.0",
"prism-react-renderer": "^2.1.0",
"process": "^0.11.10",
"querystring-es3": "^0.2.1",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rehype-katex": "7",
"remark-math": "6"
"remark-math": "6",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"url": "^0.11.4"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import * as buffer from "buffer";
import { useState } from "react";
import { chainMetadata } from "@hyperlane-xyz/registry";

import { CosmWasmCoreAdapter, MultiProtocolCore, MultiProtocolProvider } from "@hyperlane-xyz/sdk";
import { chainMetadata, chainAddresses } from "@hyperlane-xyz/registry";
import { ProtocolType, strip0x } from "@hyperlane-xyz/utils";

import ChainDropdown from './ChainDropdown';
import { useMultiProtocolProvider } from "../utils/registry";
import useIsBrowser from "@docusaurus/useIsBrowser";

// TODO: these should be in the registry, but for now we'll hardcode them.
// Once they're in the registry, we can move away from this.
const mailboxes = {
'neutron': 'neutron1sjzzd4gwkggy6hrrs8kxxatexzcuz3jecsxm3wqgregkulzj8r7qlnuef4',
'injective': 'inj1palm2wtp6urg0c6j4f2ukv5u5ahdcrqek0sapt',
const addressesOverrides = {
neutron: {
mailbox: 'neutron1sjzzd4gwkggy6hrrs8kxxatexzcuz3jecsxm3wqgregkulzj8r7qlnuef4',
},
injective: {
mailbox: 'inj1palm2wtp6urg0c6j4f2ukv5u5ahdcrqek0sapt',
}
};

const cosmosChains = Object.keys(mailboxes);

// Adding @hyperlane-xyz/utils as a dependency breaks things, so we copy the function here.
export function strip0x(hexstr: string) {
return hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr;
}

export default function CosmosMessageDelivered({
export default function NonEvmMessageDelivered({
chains,
}: {
chains: string[];
}) {
const [destinationChain, setDestinationChain] = useState<string>(cosmosChains[0]);
const isBrowser = useIsBrowser();
if (!isBrowser) {
// Difficulty polyfilling Buffer in the browser to be consumed by some Solana libs, so we do this instead
window.Buffer = buffer.Buffer;
}

const [destinationChain, setDestinationChain] = useState<string>(chains[0]);
const [messageId, setMessageId] = useState('');
const [status, setStatus] = useState('');
const multiProvider = useMultiProtocolProvider();

const onButtonClick = async () => {
const strippedMessageId = strip0x(messageId);
Expand All @@ -31,45 +44,39 @@ export default function CosmosMessageDelivered({
}

const metadata = chainMetadata[destinationChain];
const mailbox = mailboxes[destinationChain];
if (!mailbox) {
setStatus(`⛔️ No known Mailbox found for chain ${destinationChain}`);
return;
}
if (!metadata) {
setStatus(`⛔️ No known Mailbox found for chain ${destinationChain}`);
setStatus(`⛔️ No metadata found for chain ${destinationChain}`);
return;
}
const restUrl = metadata.restUrls?.[0]?.http;
if (!restUrl) {
setStatus(`⛔️ No available API set for chain ${destinationChain}`);
return;
}
const payload = {
mailbox: {
message_delivered: {
id: strippedMessageId,
}
}
};
const base64Payload = window.btoa(JSON.stringify(payload));
const url = `${restUrl}/cosmwasm/wasm/v1/contract/${mailbox}/smart/${base64Payload}`;

const multiProtocolCore = MultiProtocolCore.fromAddressesMap(
// @ts-ignore - doesn't like the types of some recent Sealevel chains and the overrides we provide
{
...chainAddresses,
...addressesOverrides,
}, multiProvider);
const core = multiProtocolCore.adapter(destinationChain);

setStatus(`⏳ Checking if message is delivered...`);
let responseJson;
console.log(`Fetching from ${url}`);

let delivered = false;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
responseJson = await response.json();
switch (metadata.protocol) {
case ProtocolType.Cosmos:
// `waitForMessageProcessed` is not implemented on cosmos -- instead we
// use the adapter directly.
const cosmosCore = core as CosmWasmCoreAdapter;
delivered = await cosmosCore.delivered(strip0x(messageId));
break;
default:
delivered = await core.waitForMessageProcessed(messageId, destinationChain, 0, 1);
break;
}
} catch (e) {
setStatus(`⛔️ Error checking message delivery: ${e}`);
return;
}
const delivered = responseJson?.data?.delivered ?? false;

if (delivered) {
setStatus(`Message successfully delivered: ✅`);
} else {
Expand All @@ -85,7 +92,7 @@ export default function CosmosMessageDelivered({
}}>
<ChainDropdown
chain={destinationChain}
chains={cosmosChains}
chains={chains}
label="Destination Chain"
onChange={setDestinationChain}
/>
Expand Down
111 changes: 111 additions & 0 deletions src/components/SealevelWarpTransferInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as buffer from "buffer";
import { useState } from "react";

import { chainMetadata } from "@hyperlane-xyz/registry";
import { addressToBytesEvm, bytesToAddressSol, ensure0x, hexOrBase58ToHex, strip0x } from "@hyperlane-xyz/utils";

import ChainDropdown from './ChainDropdown';
import { useMultiProtocolProvider } from "../utils/registry";
import useIsBrowser from "@docusaurus/useIsBrowser";

export default function NonEvmMessageDelivered({
chains,
}: {
chains: string[];
}) {
const isBrowser = useIsBrowser();
if (!isBrowser) {
// Difficulty polyfilling Buffer in the browser to be consumed by some Solana libs, so we do this instead
window.Buffer = buffer.Buffer;
}

const [originChain, setOriginChain] = useState<string>(chains[0]);
const [txId, setTxId] = useState('');
const [status, setStatus] = useState('');
const multiProvider = useMultiProtocolProvider();

const setTransferRecipient = async () => {
const metadata = chainMetadata[originChain];
if (!metadata) {
setStatus(`⛔️ No metadata found for chain ${originChain}`);
return;
}

const sealevelProvider = multiProvider.getSolanaWeb3Provider(originChain)
setStatus(`⏳ Getting transaction...`);
let tx = await sealevelProvider.getParsedTransaction(txId);
if (!tx) {
setStatus(`⛔️ Transaction not found`);
return;
}

const finalInstruction = tx.transaction.message.instructions.length - 1;
// Types don't all include `data`, but in practice we have it.
const transferRemoteInstruction: any = tx.transaction.message.instructions[finalInstruction];
const transferRemoteData = transferRemoteInstruction.data;
if (!transferRemoteData) {
setStatus(`⛔️ No data found in instruction`);
return;
}

const hex = strip0x(hexOrBase58ToHex(transferRemoteData));
// The first 13 bytes (2 chars per byte) are the instruction discriminator. After that, we have the 32 byte recipient address.
const recipientHex = ensure0x(hex.slice(26, 26 + 64));
const recipientBytes = addressToBytesEvm(recipientHex);
const recipientBase58 = bytesToAddressSol(recipientBytes);

setStatus(`🕵️‍♂️ Transfer recipient (hex): ${recipientHex}\n🕵️‍♂️ Transfer recipient (base58): ${recipientBase58}`);
};

const onButtonClick = async () => {
try {
await setTransferRecipient();
} catch (e) {
setStatus(`⛔️ Error: ${e.message}`);
}
}

return (
<div style={{
padding: "10px",
borderRadius: "5px",
border: "1px solid #ccc",
}}>
<ChainDropdown
chain={originChain}
chains={chains}
label="Destination Chain"
onChange={setOriginChain}
/>
<div>
Origin tx id (signature):{"\t"}
<input
defaultValue={txId}
onChange={(e) => setTxId(e.target.value)}
placeholder="Abc..."
style={{
width: "100%",
padding: "5px",
borderRadius: "5px",
border: "1px solid #ccc",
fontSize: "1.05em",
}}
/>
</div>
<button
onClick={onButtonClick}
style={{
padding: "10px",
borderRadius: "5px",
border: "1px solid #ccc",
marginTop: "10px",
fontSize: "1em",
}}
className="button button--secondary"
>Get transfer recipient</button>
<div style={{ margin: "10px", whiteSpace: 'pre-wrap' }}>
{status}
</div>
</div>
);
}
15 changes: 14 additions & 1 deletion src/utils/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { chainAddresses, chainMetadata } from "@hyperlane-xyz/registry";
import type { ChainMetadata, ChainName } from "@hyperlane-xyz/sdk";
import { MultiProtocolProvider, type ChainMetadata, type ChainName } from "@hyperlane-xyz/sdk";
import { deepCopy, objMerge } from "@hyperlane-xyz/utils";
import { useMemo } from "react";

const ABACUS_WORKS_DEPLOYER_NAME = "abacus works";
Expand Down Expand Up @@ -30,3 +31,15 @@ export function getAbacusWorksChainNames(isTestnet = false): ChainName[] {
export function useAbacusWorksChainNames(isTestnet = false) {
return useMemo(() => getAbacusWorksChainNames(isTestnet), [isTestnet]);
}

export function useMultiProtocolProvider() {
return useMemo(() => {
const chainMetadataCopied = deepCopy(chainMetadata);
// The default public Solana RPC aggressively rate limits, so we add another public node to the list
chainMetadataCopied.solanamainnet.rpcUrls.unshift({
http: 'https://solana-rpc.publicnode.com'
});

return new MultiProtocolProvider(chainMetadataCopied);
}, []);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions webpackPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import webpack from 'webpack';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

module.exports = function (_context, _options) {
return {
name: 'custom-docusaurus-plugin',
configureWebpack(_config, _isServer, _utils) {
return {
resolve: {
// Various libraries required by Solana tooling
fallback: {
"crypto": require.resolve("crypto-browserify"),
"zlib": require.resolve("browserify-zlib"),
"url": require.resolve("url/"),
"https": require.resolve("https-browserify"),
"http": require.resolve("stream-http"),
"querystring": require.resolve("querystring-es3"),
"stream": require.resolve("stream-browserify"),
"buffer": require.resolve("buffer/"),
"vm": false,
},
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
]
};
},
};
};
Loading