@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
172 lines • 9.18 kB
JavaScript
import { ethers } from 'ethers';
import { CircleBridgeAdapter__factory, ICircleMessageTransmitter__factory, ITokenMessenger__factory, Mailbox__factory, PortalAdapter__factory, } from '@hyperlane-xyz/core';
import { addressToBytes32, ensure0x, eqAddress, rootLogger, strip0x, } from '@hyperlane-xyz/utils';
import { HyperlaneApp } from '../../app/HyperlaneApp.js';
import { fetchWithTimeout } from '../../utils/fetch.js';
const logger = rootLogger.child({ module: 'LiquidityLayerApp' });
const PORTAL_VAA_SERVICE_TESTNET_BASE_URL = 'https://wormhole-v2-testnet-api.certus.one/v1/signed_vaa/';
const CIRCLE_ATTESTATIONS_TESTNET_BASE_URL = 'https://iris-api-sandbox.circle.com/attestations/';
const CIRCLE_ATTESTATIONS_MAINNET_BASE_URL = 'https://iris-api.circle.com/attestations/';
const PORTAL_VAA_SERVICE_SUCCESS_CODE = 5;
const TokenMessengerInterface = ITokenMessenger__factory.createInterface();
const CircleBridgeAdapterInterface = CircleBridgeAdapter__factory.createInterface();
const PortalAdapterInterface = PortalAdapter__factory.createInterface();
const MailboxInterface = Mailbox__factory.createInterface();
const BridgedTokenTopic = CircleBridgeAdapterInterface.getEventTopic(CircleBridgeAdapterInterface.getEvent('BridgedToken'));
const PortalBridgedTokenTopic = PortalAdapterInterface.getEventTopic(PortalAdapterInterface.getEvent('BridgedToken'));
export class LiquidityLayerApp extends HyperlaneApp {
contractsMap;
multiProvider;
config;
constructor(contractsMap, multiProvider, config) {
super(contractsMap, multiProvider);
this.contractsMap = contractsMap;
this.multiProvider = multiProvider;
this.config = config;
}
async fetchCircleMessageTransactions(chain) {
logger.info(`Fetch circle messages for ${chain}`);
const url = new URL(this.multiProvider.getExplorerApiUrl(chain));
url.searchParams.set('module', 'logs');
url.searchParams.set('action', 'getLogs');
url.searchParams.set('address', this.getContracts(chain).circleBridgeAdapter.address);
url.searchParams.set('topic0', BridgedTokenTopic);
const req = await fetchWithTimeout(url);
const response = await req.json();
return response.result.map((tx) => tx.transactionHash).flat();
}
async fetchPortalBridgeTransactions(chain) {
const url = new URL(this.multiProvider.getExplorerApiUrl(chain));
url.searchParams.set('module', 'logs');
url.searchParams.set('action', 'getLogs');
url.searchParams.set('address', this.getContracts(chain).portalAdapter.address);
url.searchParams.set('topic0', PortalBridgedTokenTopic);
const req = await fetchWithTimeout(url);
const response = await req.json();
if (!response.result) {
throw Error(`Expected result in response: ${response}`);
}
return response.result.map((tx) => tx.transactionHash).flat();
}
async parsePortalMessages(chain, txHash) {
const provider = this.multiProvider.getProvider(chain);
const receipt = await provider.getTransactionReceipt(txHash);
const matchingLogs = receipt.logs
.map((log) => {
try {
return [PortalAdapterInterface.parseLog(log)];
}
catch {
return [];
}
})
.flat();
if (matchingLogs.length == 0)
return [];
const event = matchingLogs.find((log) => log.name === 'BridgedToken');
const portalSequence = event.args.portalSequence.toNumber();
const nonce = event.args.nonce.toNumber();
const destination = this.multiProvider.getChainName(event.args.destination);
return [{ origin: chain, nonce, portalSequence, destination }];
}
async parseCircleMessages(chain, txHash) {
logger.debug(`Parse Circle messages for chain ${chain} ${txHash}`);
const provider = this.multiProvider.getProvider(chain);
const receipt = await provider.getTransactionReceipt(txHash);
const matchingLogs = receipt.logs
.map((log) => {
try {
return [TokenMessengerInterface.parseLog(log)];
}
catch {
try {
return [CircleBridgeAdapterInterface.parseLog(log)];
}
catch {
try {
return [MailboxInterface.parseLog(log)];
}
catch {
return [];
}
}
}
})
.flat();
if (matchingLogs.length == 0)
return [];
const message = matchingLogs.find((log) => log.name === 'MessageSent')
.args.message;
const nonce = matchingLogs.find((log) => log.name === 'BridgedToken').args
.nonce;
const destinationDomain = matchingLogs.find((log) => log.name === 'Dispatch').args.destination;
const remoteChain = this.multiProvider.getChainName(destinationDomain);
const domain = this.config[chain].circle.circleDomainMapping.find((mapping) => mapping.hyperlaneDomain === this.multiProvider.getDomainId(chain)).circleDomain;
return [
{
chain,
remoteChain,
txHash,
message,
nonce,
domain,
nonceHash: ethers.utils.solidityKeccak256(['uint32', 'uint64'], [domain, nonce]),
},
];
}
async attemptPortalTransferCompletion(message) {
const destinationPortalAdapter = this.getContracts(message.destination)
.portalAdapter;
const transferId = await destinationPortalAdapter.transferId(this.multiProvider.getDomainId(message.origin), message.nonce);
const transferTokenAddress = await destinationPortalAdapter.portalTransfersProcessed(transferId);
if (!eqAddress(transferTokenAddress, ethers.constants.AddressZero)) {
logger.info(`Transfer with nonce ${message.nonce} from ${message.origin} to ${message.destination} already processed`);
return;
}
const wormholeOriginDomain = this.config[message.destination].portal.wormholeDomainMapping.find((mapping) => mapping.hyperlaneDomain ===
this.multiProvider.getDomainId(message.origin))?.wormholeDomain;
const emitter = strip0x(addressToBytes32(this.config[message.origin].portal.portalBridgeAddress));
const vaa = await fetchWithTimeout(`${PORTAL_VAA_SERVICE_TESTNET_BASE_URL}${wormholeOriginDomain}/${emitter}/${message.portalSequence}`).then((response) => response.json());
if (vaa.code && vaa.code === PORTAL_VAA_SERVICE_SUCCESS_CODE) {
logger.info(`VAA not yet found for nonce ${message.nonce}`);
return;
}
logger.debug(`Complete portal transfer for nonce ${message.nonce} on ${message.destination}`);
try {
await this.multiProvider.handleTx(message.destination, destinationPortalAdapter.completeTransfer(ensure0x(Buffer.from(vaa.vaaBytes, 'base64').toString('hex'))));
}
catch (error) {
if (error?.error?.reason?.includes('no wrapper for this token')) {
logger.info('No wrapper for this token, you should register the token at https://wormhole-foundation.github.io/example-token-bridge-ui/#/register');
logger.info(message);
return;
}
throw error;
}
}
async attemptCircleAttestationSubmission(message) {
const signer = this.multiProvider.getSigner(message.remoteChain);
const transmitter = ICircleMessageTransmitter__factory.connect(this.config[message.remoteChain].circle.messageTransmitterAddress, signer);
const alreadyProcessed = await transmitter.usedNonces(message.nonceHash);
if (alreadyProcessed) {
logger.info(`Message sent on ${message.txHash} was already processed`);
return;
}
logger.info(`Attempt Circle message delivery`, JSON.stringify(message));
const messageHash = ethers.utils.keccak256(message.message);
const baseurl = this.multiProvider.getChainMetadata(message.chain).isTestnet
? CIRCLE_ATTESTATIONS_TESTNET_BASE_URL
: CIRCLE_ATTESTATIONS_MAINNET_BASE_URL;
const attestationsB = await fetchWithTimeout(`${baseurl}${messageHash}`);
const attestations = await attestationsB.json();
if (attestations.status !== 'complete') {
logger.info(`Attestations not available for message nonce ${message.nonce} on ${message.txHash}`);
return;
}
logger.info(`Ready to submit attestations for message ${message.nonce}`);
const tx = await transmitter.receiveMessage(message.message, attestations.attestation);
logger.info(`Submitted attestations in ${this.multiProvider.tryGetExplorerTxUrl(message.remoteChain, tx)}`);
await this.multiProvider.handleTx(message.remoteChain, tx);
}
}
//# sourceMappingURL=LiquidityLayerApp.js.map