Skip to content
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
46 changes: 27 additions & 19 deletions src/adapter/bridges/BridgeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ import {
BRIDGE_API_DESTINATION_TOKEN_SYMBOLS,
roundAmountToSend,
mapAsync,
createFormatFunction,
floatToBN,
ZERO_BYTES,
} from "../../utils";
import { TransferTokenParams } from "../utils";
import ERC20_ABI from "../../common/abi/MinimalERC20.json";

export class BridgeApi extends BaseBridgeAdapter {
protected api: BridgeApiClient;
protected l1TokenInfo: TokenInfo;
protected dstCurrency: string;

constructor(
l2chainId: number,
Expand All @@ -47,7 +51,7 @@ export class BridgeApi extends BaseBridgeAdapter {
this.l2Bridge = new Contract(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], ERC20_ABI, l2SignerOrProvider);

// We need to fetch some API configuration details from environment.
const { BRIDGE_API_BASE, BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env;
const { BRIDGE_API_BASE = "https://api.bridge.xyz", BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env;

assert(isDefined(BRIDGE_API_BASE), "BRIDGE_API_BASE must be set in the environment");
assert(isDefined(BRIDGE_API_KEY), "BRIDGE_API_KEY must be set in the environment");
Expand All @@ -63,14 +67,14 @@ export class BridgeApi extends BaseBridgeAdapter {
);

this.l1TokenInfo = getTokenInfo(l1Token, this.hubChainId);
this.dstCurrency = BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[this.getL2Bridge().address];
}

async constructL1ToL2Txn(
toAddress: Address,
_l1Token: EvmAddress,
l2Token: Address,
_amount: BigNumber,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_optionalParams?: TransferTokenParams
): Promise<BridgeTransactionDetails> {
const amount = roundAmountToSend(_amount, this.l1TokenInfo.decimals, 2); // The bridge API only deals with values up to 2 decimals.
Expand All @@ -82,10 +86,12 @@ export class BridgeApi extends BaseBridgeAdapter {
if (amount.lt(BRIDGE_API_MINIMUMS[this.hubChainId]?.[this.l2chainId] ?? toBN(Number.MAX_SAFE_INTEGER))) {
throw new Error(`Cannot bridge to ${getNetworkName(this.l2chainId)} due to invalid amount ${amount}`);
}
const formatter = createFormatFunction(2, 4, false, this.l1TokenInfo.decimals);
const transferRouteAddress = await this.api.createTransferRouteEscrowAddress(
toAddress,
this.l1TokenInfo.symbol,
BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[this.getL2Bridge().address]
BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[this.getL2Bridge().address],
formatter(amount)
);
return Promise.resolve({
contract: this.getL1Bridge(),
Expand All @@ -96,7 +102,7 @@ export class BridgeApi extends BaseBridgeAdapter {

async queryL1BridgeInitiationEvents(
_l1Token: EvmAddress,
_fromAddress: EvmAddress,
fromAddress: EvmAddress,
toAddress: Address,
eventConfig: EventSearchConfig
): Promise<BridgeEvents> {
Expand All @@ -110,24 +116,30 @@ export class BridgeApi extends BaseBridgeAdapter {
statusesGrouped,
});

const initialPendingRebalances = await this.api.filterInitiatedTransfers(
pendingTransfers.filter((pendingTransfer) => pendingTransfer.destination.currency === this.dstCurrency),
fromAddress,
eventConfig,
this.l1Signer.provider
);
const pendingRebalances = await mapAsync(
pendingTransfers.filter((pendingTransfer) => {
const destinationAddress = toAddressType(pendingTransfer.destination.to_address, this.l2chainId);
initialPendingRebalances.filter(({ destination, source_deposit_instructions }) => {
const destinationAddress = toAddressType(destination.to_address, this.l2chainId);
return (
destinationAddress.eq(toAddress) &&
pendingTransfer.state !== "awaiting_funds" &&
pendingTransfer.state !== "payment_processed" &&
pendingTransfer.source_deposit_instructions.currency === this.l1TokenInfo.symbol.toLowerCase()
source_deposit_instructions.currency === this.l1TokenInfo.symbol.toLowerCase()
);
}),
async ({ receipt }) => {
const transaction = await this.l1Signer.provider.getTransactionReceipt(receipt.source_tx_hash);
async (pendingTransfer) => {
const transaction = isDefined(pendingTransfer?.receipt?.source_tx_hash)
? await this.l1Signer.provider.getTransactionReceipt(pendingTransfer.receipt.source_tx_hash)
: undefined;
return {
txnRef: receipt.source_tx_hash,
txnRef: pendingTransfer.receipt?.source_tx_hash ?? ZERO_BYTES,
logIndex: 0, // logIndex is zero since the only call for initiation is a `Transfer`.
txnIndex: transaction?.transactionIndex,
blockNumber: transaction?.blockNumber,
amount: toBN(Math.floor(Number(receipt.final_amount) * 10 ** this.l1TokenInfo.decimals)),
txnIndex: transaction?.transactionIndex ?? 0,
blockNumber: transaction?.blockNumber ?? 0,
amount: floatToBN(pendingTransfer.receipt?.final_amount ?? pendingTransfer.amount, this.l1TokenInfo.decimals),
};
}
);
Expand All @@ -137,13 +149,9 @@ export class BridgeApi extends BaseBridgeAdapter {
}

async queryL2BridgeFinalizationEvents(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_l1Token: EvmAddress,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_fromAddress: EvmAddress,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_toAddress: Address,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_eventConfig: EventSearchConfig
): Promise<BridgeEvents> {
return Promise.resolve({});
Expand Down
9 changes: 9 additions & 0 deletions src/adapter/l2Bridges/BaseL2BridgeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Address,
getHubPoolAddress,
getSpokePoolAddress,
getTranslatedTokenAddress,
SVMProvider,
SolanaTransaction,
isDefined,
Expand Down Expand Up @@ -48,6 +49,14 @@ export abstract class BaseL2BridgeAdapter {
return DEFAULT_PENDING_WITHDRAWAL_LOOKBACK_PERIOD_SECONDS;
}

/**
* Returns the L2 token address this bridge operates on.
* Override in subclasses that use a non-canonical L2 token (e.g. BridgeApi using pathUSD).
*/
getL2Token(): Address {
return getTranslatedTokenAddress(this.l1Token, this.hubChainId, this.l2chainId);
}

abstract constructWithdrawToL1Txns(
toAddress: Address,
l2Token: Address,
Expand Down
126 changes: 126 additions & 0 deletions src/adapter/l2Bridges/BridgeApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
Address,
BigNumber,
bnZero,
Contract,
EventSearchConfig,
getNetworkName,
Signer,
EvmAddress,
CHAIN_IDs,
BRIDGE_API_MINIMUMS,
toBN,
BridgeApiClient,
getTokenInfo,
floatToBN,
isDefined,
assert,
BRIDGE_API_DESTINATION_TOKEN_SYMBOLS,
getTimestampForBlock,
toAddressType,
BRIDGE_API_DESTINATION_TOKENS,
createFormatFunction,
roundAmountToSend,
} from "../../utils";
import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter";
import { AugmentedTransaction } from "../../clients/TransactionClient";
import { TokenInfo } from "../../interfaces";
import ERC20_ABI from "../../common/abi/MinimalERC20.json";

export class BridgeApi extends BaseL2BridgeAdapter {
protected api: BridgeApiClient;
protected l1TokenInfo: TokenInfo;
protected l2TokenInfo: TokenInfo;

constructor(l2chainId: number, hubChainId: number, l2Signer: Signer, l1Signer: Signer, l1Token: EvmAddress) {
if (hubChainId !== CHAIN_IDs.MAINNET) {
throw new Error("Cannot define a Bridge API bridge for a non-production network");
}
super(l2chainId, hubChainId, l2Signer, l1Signer, l1Token);

// We need to fetch some API configuration details from environment.
const { BRIDGE_API_BASE = "https://api.bridge.xyz", BRIDGE_API_KEY, BRIDGE_CUSTOMER_ID } = process.env;

assert(isDefined(BRIDGE_API_BASE), "BRIDGE_API_BASE must be set in the environment");
assert(isDefined(BRIDGE_API_KEY), "BRIDGE_API_KEY must be set in the environment");
assert(isDefined(BRIDGE_CUSTOMER_ID), "BRIDGE_CUSTOMER_ID must be set in the environment");

this.api = new BridgeApiClient(
BRIDGE_API_BASE,
BRIDGE_API_KEY,
BRIDGE_CUSTOMER_ID,
this.l2chainId,
this.hubChainId
);

this.l1TokenInfo = getTokenInfo(l1Token, this.hubChainId);
this.l2TokenInfo = getTokenInfo(
toAddressType(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], this.l2chainId),
this.l2chainId
);
}

override getL2Token(): Address {
return toAddressType(BRIDGE_API_DESTINATION_TOKENS[this.l2chainId], this.l2chainId);
}

async constructWithdrawToL1Txns(
toAddress: EvmAddress,
l2Token: EvmAddress,
l1Token: EvmAddress,
_amount: BigNumber
): Promise<AugmentedTransaction[]> {
const amount = roundAmountToSend(_amount, this.l2TokenInfo.decimals, 2);
// If amount is less than the network minimums, then throw.
if (amount.lt(BRIDGE_API_MINIMUMS[this.l2chainId]?.[this.hubChainId] ?? toBN(Number.MAX_SAFE_INTEGER))) {
throw new Error(`Cannot bridge to ${getNetworkName(this.hubChainId)} due to invalid amount ${amount}`);
}
const formatter = createFormatFunction(2, 4, false, this.l2TokenInfo.decimals);
const l2TokenSymbol = BRIDGE_API_DESTINATION_TOKEN_SYMBOLS[l2Token.toNative()];
const transferRouteSource = await this.api.createTransferRouteEscrowAddress(
toAddress,
l2TokenSymbol,
this.l1TokenInfo.symbol,
formatter(amount)
);
const l2TokenContract = new Contract(l2Token.toNative(), ERC20_ABI, this.l2Signer);
const transferTxn = {
contract: l2TokenContract,
method: "transfer",
chainId: this.l2chainId,
args: [transferRouteSource, amount],
nonMulticall: true,
canFailInSimulation: false,
value: bnZero,
message: `🎰 Withdrew ${getNetworkName(this.l2chainId)} ${this.l2TokenInfo.symbol} to L1`,
mrkdwn: `Withdrew ${formatter(amount.toString())} ${this.l2TokenInfo.symbol} from ${getNetworkName(
this.l2chainId
)} to L1`,
};
return [transferTxn];
}

// @dev We do not filter on origin/destination tokens since there is only one bridge API destination token for any destination chain.
// e.g. For Tempo, we only use Bridge for pathUSD; other tokens are rebalanced via other methods, so
// if there is an outstanding transfer from Tempo to Ethereum, then this must be a pathUSD transfer.
async getL2PendingWithdrawalAmount(
l2EventConfig: EventSearchConfig,
l1EventConfig: EventSearchConfig,
fromAddress: EvmAddress,
l2Token: EvmAddress
): Promise<BigNumber> {
const fromTimestamp = await getTimestampForBlock(this.l1Signer.provider, l1EventConfig.from);
const allTransfers = await this.api.getAllTransfersInRange(fromAddress, fromTimestamp * 1000);

const allInitiatedTransfers = await this.api.filterInitiatedTransfers(
allTransfers,
fromAddress,
l2EventConfig,
this.l2Signer.provider
);
return allInitiatedTransfers.reduce((acc, transfer) => {
const { decimals: l2TokenDecimals } = getTokenInfo(l2Token, this.l2chainId);
return acc.add(floatToBN(transfer.receipt?.final_amount ?? transfer.amount, l2TokenDecimals));
}, bnZero);
}
}
68 changes: 68 additions & 0 deletions src/adapter/l2Bridges/TokenSplitterBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
BigNumber,
ConvertDecimals,
EventSearchConfig,
getTokenInfo,
Signer,
EvmAddress,
getTranslatedTokenAddress,
SolanaTransaction,
} from "../../utils";
import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter";
import { AugmentedTransaction } from "../../clients/TransactionClient";
import { L2_TOKEN_SPLITTER_BRIDGES } from "../../common";

export class TokenSplitterBridge extends BaseL2BridgeAdapter {
protected bridge1;
protected bridge2;

constructor(l2chainId: number, hubChainId: number, l2Signer: Signer, l1Signer: Signer, l1Token: EvmAddress) {
super(l2chainId, hubChainId, l2Signer, l1Signer, l1Token);

const [bridge1Constructor, bridge2Constructor] = L2_TOKEN_SPLITTER_BRIDGES[this.l2chainId][this.l1Token.toNative()];
this.bridge1 = new bridge1Constructor(l2chainId, hubChainId, l2Signer, l1Signer, l1Token);
this.bridge2 = new bridge2Constructor(l2chainId, hubChainId, l2Signer, l1Signer, l1Token);
}

getRouteForL2Token(l2Token: EvmAddress): BaseL2BridgeAdapter {
return getTranslatedTokenAddress(this.l1Token, this.hubChainId, this.l2chainId).eq(l2Token)
? this.bridge1
: this.bridge2;
}

async constructWithdrawToL1Txns(
toAddress: EvmAddress,
l2Token: EvmAddress,
l1Token: EvmAddress,
amount: BigNumber
): Promise<AugmentedTransaction[] | SolanaTransaction[]> {
return this.getRouteForL2Token(l2Token).constructWithdrawToL1Txns(toAddress, l2Token, l1Token, amount);
}

async getL2PendingWithdrawalAmount(
l2EventConfig: EventSearchConfig,
l1EventConfig: EventSearchConfig,
fromAddress: EvmAddress,
l2Token: EvmAddress
): Promise<BigNumber> {
const [bridge1Pending, bridge2Pending] = await Promise.all([
this.bridge1.getL2PendingWithdrawalAmount(l2EventConfig, l1EventConfig, fromAddress, l2Token),
this.bridge2.getL2PendingWithdrawalAmount(l2EventConfig, l1EventConfig, fromAddress, l2Token),
]);

// Each bridge may return amounts denominated in its own L2 token's decimals.
// Normalize both to the decimals of the requested l2Token before summing.
const targetDecimals = getTokenInfo(l2Token, this.l2chainId).decimals;
const bridge1Decimals = getTokenInfo(this.bridge1.getL2Token(), this.l2chainId).decimals;
const bridge2Decimals = getTokenInfo(this.bridge2.getL2Token(), this.l2chainId).decimals;

const normalizedBridge1 = ConvertDecimals(bridge1Decimals, targetDecimals)(bridge1Pending);
const normalizedBridge2 = ConvertDecimals(bridge2Decimals, targetDecimals)(bridge2Pending);

return normalizedBridge1.add(normalizedBridge2);
}

public override requiredTokenApprovals(): { token: EvmAddress; bridge: EvmAddress }[] {
return [...this.bridge1.requiredTokenApprovals(), ...this.bridge2.requiredTokenApprovals()];
}
}
2 changes: 2 additions & 0 deletions src/adapter/l2Bridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from "./OpStackUSDCBridge";
export * from "./OpStackWethBridge";
export * from "./UsdcCCTPBridge";
export * from "./SolanaUsdcCCTPBridge";
export * from "./BridgeApi";
export * from "./TokenSplitterBridge";
13 changes: 12 additions & 1 deletion src/common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
UsdcCCTPBridge as L2UsdcCCTPBridge,
BinanceCEXNativeBridge as L2BinanceCEXNativeBridge,
SolanaUsdcCCTPBridge as L2SolanaUsdcCCTPBridge,
BridgeApi as L2BridgeApi,
TokenSplitterBridge as L2TokenSplitterBridge,
} from "../adapter/l2Bridges";
import { CONTRACT_ADDRESSES } from "./ContractAddresses";
import { HyperlaneXERC20Bridge } from "../adapter/bridges/HyperlaneXERC20Bridge";
Expand Down Expand Up @@ -607,6 +609,15 @@ export const TOKEN_SPLITTER_BRIDGES: Record<
},
};

export const L2_TOKEN_SPLITTER_BRIDGES: Record<
number,
Record<string, [L2BridgeConstructor<BaseL2BridgeAdapter>, L2BridgeConstructor<BaseL2BridgeAdapter>]>
> = {
[CHAIN_IDs.TEMPO]: {
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: [OFTL2Bridge, L2BridgeApi],
},
};

export const CUSTOM_L2_BRIDGE: Record<number, Record<string, L2BridgeConstructor<BaseL2BridgeAdapter>>> = {
[CHAIN_IDs.LISK]: {
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: L2OpStackUSDCBridge,
Expand Down Expand Up @@ -657,7 +668,7 @@ export const CUSTOM_L2_BRIDGE: Record<number, Record<string, L2BridgeConstructor
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: L2BinanceCEXNativeBridge,
},
[CHAIN_IDs.TEMPO]: {
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: OFTL2Bridge,
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: L2TokenSplitterBridge,
},
[CHAIN_IDs.UNICHAIN]: {
[TOKEN_SYMBOLS_MAP.ezETH.addresses[CHAIN_IDs.MAINNET]]: HyperlaneXERC20BridgeL2,
Expand Down
Loading
Loading