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(bridge-ui-v2): transaction logic #14295

Merged
merged 12 commits into from
Jul 28, 2023
4 changes: 4 additions & 0 deletions packages/bridge-ui-v2/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ export const storageService = {
bridgeTxPrefix: 'bridge-tx',
customTokenPrefix: 'custom-token',
};

export const bridgeTransactionPoller = {
interval: 20_000,
};
127 changes: 127 additions & 0 deletions packages/bridge-ui-v2/src/libs/bridge/bridgeTxMessageStatusPoller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { getContract } from '@wagmi/core';
import { EventEmitter } from 'events';

import { bridgeABI } from '$abi';
import { bridgeTransactionPoller } from '$config';
import { chainContractsMap } from '$libs/chain';
import { BridgeTxPollingError } from '$libs/error';
import { getLogger } from '$libs/util/logger';
import { nextTick } from '$libs/util/nextTick';

import { isBridgeTxProcessable } from './isBridgeTxProcessable';
import { type BridgeTransaction, MessageStatus } from './types';

const log = getLogger('bridge:bridgeTxMessageStatusPoller');

export enum PollingEvent {
STOP = 'stop',
STATUS = 'status', // emits MessageStatus

// Whether or not the tx can be clamied/retried/released
PROCESSABLE = 'processable',
}

const intervalEmitterMap: Record<number, EventEmitter> = {};

/**
* @example:
* try {
* const emitter = startPolling(bridgeTx);
*
* if(emitter) {
* emitter.on(PollingEvent.STOP, () => {});
* emitter.on(PollingEvent.STATUS, (status: MessageStatus) => {});
* emitter.on(PollingEvent.PROCESSABLE, (isProcessable: boolean) => {});
* }
* } catch (err) {
* // something really bad with this bridgeTx
* }
*/
export function startPolling(bridgeTx: BridgeTransaction, runImmediately = true): Maybe<EventEmitter> {
const { destChainId, msgHash, status } = bridgeTx;

// Without this we cannot poll at all. Let's throw an error
// that can be handled in the UI
if (!msgHash) {
throw new BridgeTxPollingError('missing msgHash');
}

// It could happen that the transaction has already been claimed
// by the time we want to start polling, in which case we're already done
if (status === MessageStatus.DONE) return;

// We want to notify whoever is calling this function of different
// events: PollingEvent
const emitter = new EventEmitter();

const stopPolling = () => {
if (bridgeTx.interval) {
log('Stop polling for transaction', bridgeTx);

// Clean up
clearInterval(bridgeTx.interval);
delete intervalEmitterMap[Number(bridgeTx.interval)]
bridgeTx.interval = null;

emitter.emit(PollingEvent.STOP);
}
};

const pollingFn = async () => {
const isProcessable = await isBridgeTxProcessable(bridgeTx);

emitter.emit(PollingEvent.PROCESSABLE, isProcessable);

const destBridgeAddress = chainContractsMap[Number(destChainId)].bridgeAddress;

const destBridgeContract = getContract({
address: destBridgeAddress,
abi: bridgeABI,
chainId: Number(destChainId),
});

try {
// We want to poll for status changes
const messageStatus: MessageStatus = await destBridgeContract.read.getMessageStatus([msgHash]);

bridgeTx.status = messageStatus;

emitter.emit('status', messageStatus);

if (messageStatus === MessageStatus.DONE) {
log('Poller has picked up the change of status to DONE');
stopPolling();
}
} catch (err) {
console.error(err);

stopPolling();

// 😱 UI should handle this error
throw new BridgeTxPollingError('something bad happened while polling for status', { cause: err });
}
};

if (!bridgeTx.interval) {
log('Starting polling for transaction', bridgeTx);

bridgeTx.interval = setInterval(pollingFn, bridgeTransactionPoller.interval);

intervalEmitterMap[Number(bridgeTx.interval)] = emitter;

// setImmediate isn't standard
if (runImmediately) {
// We run the polling function in the next tick so we can
// attach listeners before the polling function is called
nextTick(pollingFn);
}

return emitter;
}

log('Already polling for transaction', bridgeTx);

// We are already polling for this transaction.
// Return the emitter associated to it
return intervalEmitterMap[Number(bridgeTx.interval)];
}
41 changes: 41 additions & 0 deletions packages/bridge-ui-v2/src/libs/bridge/isBridgeTxProcessable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getContract } from '@wagmi/core';

import { crossChainSyncABI } from '$abi';
import { chainContractsMap } from '$libs/chain';
import { publicClient } from '$libs/wagmi';

import { type BridgeTransaction, MessageStatus } from './types';

export async function isBridgeTxProcessable(bridgeTx: BridgeTransaction) {
const { receipt, message, status, srcChainId, destChainId } = bridgeTx;

// Without these guys there is no way we can process this
// bridge transaction. The receipt is needed in order to compare
// the block number with the cross chain block number.
if (!receipt || !message) return false;

// TODO: Not sure this could ever happens. When we add the
// transaction to the local storage, we don't set the status,
// but when we fetch them, then we query the contract for this status.
if (status !== MessageStatus.NEW) return true;

const destCrossChainSyncAddress = chainContractsMap[Number(destChainId)].crossChainSyncAddress;

try {
const destCrossChainSyncContract = getContract({
address: destCrossChainSyncAddress,
abi: crossChainSyncABI,
chainId: Number(destChainId),
});

const blockHash = await destCrossChainSyncContract.read.getCrossChainBlockHash([BigInt(0)]);

const srcBlock = await publicClient({ chainId: Number(srcChainId) }).getBlock({
blockHash,
});

return srcBlock.number && receipt.blockNumber <= srcBlock.number;
} catch (error) {
return false;
}
}
2 changes: 2 additions & 0 deletions packages/bridge-ui-v2/src/libs/bridge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export type BridgeTransaction = {
receipt?: TransactionReceipt;
msgHash?: Hash;
message?: Message;

interval: Maybe<ReturnType<typeof setInterval>>;
};

// TokenVault sendERC20(...args)
Expand Down
4 changes: 4 additions & 0 deletions packages/bridge-ui-v2/src/libs/error/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ export class WrongOwnerError extends Error {
export class WrongChainError extends Error {
name = 'WrongChainError';
}

export class BridgeTxPollingError extends Error {
name = 'BridgeTxPollingError';
}