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: migrate to Unified Bridge, introduce the ICTT bridge #82

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@avalabs/avalanche-module": "0.11.2",
"@avalabs/avalanchejs": "4.1.0-alpha.7",
"@avalabs/bitcoin-module": "0.11.2",
"@avalabs/bridge-unified": "0.0.0-feat-ictt-configs-20241009072139",
"@avalabs/bridge-unified": "3.1.0",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.10",
"@avalabs/core-chains-sdk": "3.1.0-alpha.10",
"@avalabs/core-coingecko-sdk": "3.1.0-alpha.10",
Expand Down
12 changes: 12 additions & 0 deletions src/background/services/featureFlags/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export enum FeatureGates {
SEEEDLESS_MFA_SETTINGS = 'seedless-mfa-settings',
SEEDLESS_OPTIONAL_MFA = 'seedless-optional-mfa',
UNIFIED_BRIDGE_CCTP = 'unified-bridge-cctp',
UNIFIED_BRIDGE_ICTT = 'unified-bridge-ictt',
UNIFIED_BRIDGE_AB_EVM = 'unified-bridge-ab-evm',
UNIFIED_BRIDGE_AB_AVA_TO_BTC = 'unified-bridge-ab-ava-to-btc',
UNIFIED_BRIDGE_AB_BTC_TO_AVA = 'unified-bridge-ab-btc-to-ava',
DEBANK_TRANSACTION_PARSING = 'debank-transaction-parsing',
DEBANK_TRANSACTION_PRE_EXECUTION = 'debank-transaction-pre-execution',
PRIMARY_ACCOUNT_REMOVAL = 'primary-account-removal',
Expand Down Expand Up @@ -76,6 +80,10 @@ export const DISABLED_FLAG_VALUES: FeatureFlags = {
[FeatureGates.SEEEDLESS_MFA_SETTINGS]: false,
[FeatureGates.SEEDLESS_OPTIONAL_MFA]: false,
[FeatureGates.UNIFIED_BRIDGE_CCTP]: false,
[FeatureGates.UNIFIED_BRIDGE_ICTT]: false,
[FeatureGates.UNIFIED_BRIDGE_AB_EVM]: false,
[FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: false,
[FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: false,
[FeatureGates.DEBANK_TRANSACTION_PARSING]: false,
[FeatureGates.DEBANK_TRANSACTION_PRE_EXECUTION]: false,
[FeatureGates.PRIMARY_ACCOUNT_REMOVAL]: false,
Expand Down Expand Up @@ -121,6 +129,10 @@ export const DEFAULT_FLAGS: FeatureFlags = {
[FeatureGates.SEEEDLESS_MFA_SETTINGS]: true,
[FeatureGates.SEEDLESS_OPTIONAL_MFA]: true,
[FeatureGates.UNIFIED_BRIDGE_CCTP]: true,
[FeatureGates.UNIFIED_BRIDGE_ICTT]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true,
[FeatureGates.DEBANK_TRANSACTION_PARSING]: false,
[FeatureGates.DEBANK_TRANSACTION_PRE_EXECUTION]: false,
[FeatureGates.PRIMARY_ACCOUNT_REMOVAL]: true,
Expand Down
35 changes: 16 additions & 19 deletions src/background/services/unifiedBridge/UnifiedBridgeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => {
isMainnet: jest.fn(),
getNetwork: jest.fn(),
getProviderForNetwork: jest.fn(),
getBitcoinProvider: jest.fn(),
sendTransaction: jest.fn(),
} as any;

Expand All @@ -41,6 +42,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => {
featureFlags: {
[FeatureGates.IMPORT_FIREBLOCKS]: true,
[FeatureGates.UNIFIED_BRIDGE_CCTP]: true,
[FeatureGates.UNIFIED_BRIDGE_ICTT]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true,
},
addListener: jest.fn(),
} as any;
Expand All @@ -64,6 +69,7 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => {
networkService.getNetwork.mockImplementation(async (chainId) => ({
chainId,
}));
networkService.getBitcoinProvider.mockResolvedValue({} as any);
});

it('creates core instance with proper environment', async () => {
Expand Down Expand Up @@ -118,6 +124,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => {
// Toggle an irrelevant flag off
mockFeatureFlagChanges({
[FeatureGates.UNIFIED_BRIDGE_CCTP]: true,
[FeatureGates.UNIFIED_BRIDGE_ICTT]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true,
[FeatureGates.IMPORT_FIREBLOCKS]: false,
});

Expand All @@ -127,6 +137,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => {
// Toggle a relevant flag off
mockFeatureFlagChanges({
[FeatureGates.UNIFIED_BRIDGE_CCTP]: false,
[FeatureGates.UNIFIED_BRIDGE_ICTT]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true,
[FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true,
[FeatureGates.IMPORT_FIREBLOCKS]: false,
});

Expand Down Expand Up @@ -180,30 +194,13 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => {
});

new UnifiedBridgeService(networkService, storageService, flagsService);
await jest.runAllTimersAsync();
await jest.runAllTicks();

expect(getEnabledBridgeServices).toHaveBeenCalledTimes(1);
expect(getEnabledBridgeServices).toHaveBeenCalledTimes(4);
expect(wait).toHaveBeenNthCalledWith(1, 2000);

jest.advanceTimersByTime(2000);
await jest.runOnlyPendingTimers();
await jest.runAllTicks();

expect(getEnabledBridgeServices).toHaveBeenCalledTimes(2);
expect(wait).toHaveBeenNthCalledWith(2, 4000);

jest.advanceTimersByTime(4000);
await jest.runOnlyPendingTimers();
await jest.runAllTicks();

expect(getEnabledBridgeServices).toHaveBeenCalledTimes(3);
expect(wait).toHaveBeenNthCalledWith(3, 8000);

jest.advanceTimersByTime(8000);
await jest.runOnlyPendingTimers();
await jest.runAllTicks();

expect(getEnabledBridgeServices).toHaveBeenCalledTimes(4);
expect(createUnifiedBridgeService).toHaveBeenCalled();
});
});
Expand Down
56 changes: 47 additions & 9 deletions src/background/services/unifiedBridge/UnifiedBridgeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { singleton } from 'tsyringe';
import {
AnalyzeTxParams,
AnalyzeTxResult,
BridgeInitializer,
BridgeTransfer,
BridgeType,
createUnifiedBridgeService,
Environment,
getEnabledBridgeServices,
} from '@avalabs/bridge-unified';
import { BitcoinProvider } from '@avalabs/core-wallets-sdk';
import { wait } from '@avalabs/core-utils-sdk';
import EventEmitter from 'events';

Expand All @@ -32,6 +34,7 @@ import {
import sentryCaptureException, {
SentryExceptionTypes,
} from '@src/monitoring/sentryCaptureException';
import { getEnabledBridgeTypes } from '@src/utils/getEnabledBridgeTypes';

@singleton()
export class UnifiedBridgeService implements OnStorageReady {
Expand Down Expand Up @@ -110,16 +113,49 @@ export class UnifiedBridgeService implements OnStorageReady {
});
}

#getDisabledBridges(): BridgeType[] {
const bridges: BridgeType[] = [
BridgeType.ICTT_ERC20_ERC20,
BridgeType.AVALANCHE_EVM,
];
#getBridgeInitializers(
bitcoinProvider: BitcoinProvider
): BridgeInitializer[] {
return getEnabledBridgeTypes(this.#flagStates).map((type) =>
this.#getInitializerForBridgeType(type, bitcoinProvider)
);
}

if (!this.#flagStates[FeatureGates.UNIFIED_BRIDGE_CCTP]) {
bridges.push(BridgeType.CCTP);
#getInitializerForBridgeType(
type: BridgeType,
bitcoinProvider: BitcoinProvider
): BridgeInitializer {
// This backend service is only used for transaction tracking purposes,
// therefore we don't need to provide true signing capabilities.
const dummySigner = {
async sign() {
return '0x' as const;
},
};

switch (type) {
case BridgeType.CCTP:
case BridgeType.ICTT_ERC20_ERC20:
case BridgeType.AVALANCHE_EVM:
return {
type,
signer: dummySigner,
};

case BridgeType.AVALANCHE_AVA_BTC:
return {
type,
signer: dummySigner,
bitcoinFunctions: bitcoinProvider,
};

case BridgeType.AVALANCHE_BTC_AVA:
return {
type,
signer: dummySigner,
bitcoinFunctions: bitcoinProvider,
};
}
return bridges;
}

async #recreateService() {
Expand All @@ -128,11 +164,13 @@ export class UnifiedBridgeService implements OnStorageReady {
: Environment.TEST;

try {
const bitcoinProvider = await this.networkService.getBitcoinProvider();

this.#core = createUnifiedBridgeService({
environment,
enabledBridgeServices: await getEnabledBridgeServices(
environment,
this.#getDisabledBridges()
this.#getBridgeInitializers(bitcoinProvider)
),
});
this.#failedInitAttempts = 0;
Expand Down
9 changes: 8 additions & 1 deletion src/background/services/unifiedBridge/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ export enum UnifiedBridgeError {
InvalidFee = 'invalid-fee',
UnsupportedNetwork = 'unsupported-network',
InvalidTxPayload = 'invalid-tx-payload',
NonBitcoinAccount = 'non-bitcoin-account',
}

export type UnifiedBridgeState = {
pendingTransfers: Record<string, BridgeTransfer>;
};

export const UNIFIED_BRIDGE_TRACKED_FLAGS = [FeatureGates.UNIFIED_BRIDGE_CCTP];
export const UNIFIED_BRIDGE_TRACKED_FLAGS = [
FeatureGates.UNIFIED_BRIDGE_CCTP,
FeatureGates.UNIFIED_BRIDGE_ICTT,
FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC,
FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA,
FeatureGates.UNIFIED_BRIDGE_AB_EVM,
];

export const UNIFIED_BRIDGE_DEFAULT_STATE: UnifiedBridgeState = {
pendingTransfers: {},
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/ContainedDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const ContainedDropdown = ({
width: width ?? '100%',
borderRadius: borderRadius ?? spacing(0, 0, 1, 1),
margin: margin ?? '0',
height: isOpen ? `${height || calculatedHeight}px` : 0,
height: isOpen ? `${height || calculatedHeight - top}px` : 0,
top,
opacity: isOpen ? 1 : 0,
}}
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/CustomFees.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export function CustomFees({
<Stack sx={{ py: 0.5 }}>
<Typography variant="caption" sx={{ color: 'error.main' }}>
<Trans
i18nKey="Insufficient balance to cover gas costs. <br />Please add {{tokenSymbol}}."
i18nKey="Insufficient balance to cover gas costs. Please add {{tokenSymbol}}."
values={{ tokenSymbol: network?.networkToken.symbol }}
/>
</Typography>
Expand Down
6 changes: 4 additions & 2 deletions src/components/common/TokenEllipsis.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { PropsWithChildren } from 'react';
import { Tooltip } from '@avalabs/core-k2-components';
import { SxProps, Tooltip } from '@avalabs/core-k2-components';
import { truncateAddress } from '@avalabs/core-utils-sdk';

interface TokenEllipsisProps {
maxLength: number;
text: string;
className?: string;
sx?: SxProps;
}

function isTruncated(maxLength, text) {
Expand All @@ -16,6 +17,7 @@ export function TokenEllipsis({
maxLength,
text,
className,
sx,
}: PropsWithChildren<TokenEllipsisProps>) {
const name =
text.length <= maxLength ? text : truncateAddress(text, maxLength / 2);
Expand All @@ -26,7 +28,7 @@ export function TokenEllipsis({
title={text}
disableHoverListener={!isTruncated(maxLength, text)}
disableFocusListener={!isTruncated(maxLength, text)}
sx={{ cursor: 'pointer' }}
sx={sx ?? { cursor: 'pointer' }}
>
<>{name}</>
</Tooltip>
Expand Down
25 changes: 8 additions & 17 deletions src/components/common/TokenSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from 'react';
import { useSettingsContext } from '@src/contexts/SettingsProvider';
import { ContainedDropdown } from '@src/components/common/ContainedDropdown';
import { AssetBalance } from '@src/pages/Bridge/models';
import EthLogo from '@src/images/tokens/eth.png';
import {
hasUnconfirmedBTCBalance,
Expand Down Expand Up @@ -45,7 +44,6 @@ const InputContainer = styled(Card)`
align-items: center;
padding: 8px 16px;
background: ${({ theme }) => theme.palette.grey[850]};
cursor: pointer;
display: flex;
`;

Expand All @@ -71,7 +69,7 @@ const StyledDropdownMenuItem = styled(DropdownItem)`

interface TokenSelectProps {
selectedToken?: TokenWithBalance | null;
onTokenChange(token: TokenWithBalance | AssetBalance): void;
onTokenChange(token: TokenWithBalance): void;
maxAmount?: bigint;
inputAmount?: bigint;
onInputAmountChange?(data: { amount: string; bigint: bigint }): void;
Expand All @@ -83,7 +81,6 @@ interface TokenSelectProps {
label?: string;
selectorLabel?: string;
tokensList?: TokenWithBalance[];
bridgeTokensList?: AssetBalance[];
isValueLoading?: boolean;
hideErrorMessage?: boolean;
skipHandleMaxAmount?: boolean;
Expand All @@ -107,7 +104,6 @@ export function TokenSelect({
isValueLoading,
hideErrorMessage,
skipHandleMaxAmount,
bridgeTokensList,
setIsOpen,
containerRef,
withMaxButton = true,
Expand Down Expand Up @@ -140,13 +136,10 @@ export function TokenSelect({
},
[onInputAmountChange, maxAmountString]
);
const hideTokenDropdown =
(bridgeTokensList && bridgeTokensList.length < 2) ||
(tokensList && tokensList.length < 2);
const hideTokenDropdown = tokensList && tokensList.length < 2;

const displayTokenList = useDisplaytokenlist({
tokensList,
bridgeTokensList,
searchQuery,
});

Expand Down Expand Up @@ -187,9 +180,8 @@ export function TokenSelect({

useEffect(() => {
// when only one token is present, auto select it
const tokens = bridgeTokensList ?? tokensList;
const hasOnlyOneToken = tokens?.length === 1;
const theOnlyToken = hasOnlyOneToken ? tokens[0] : undefined;
const hasOnlyOneToken = tokensList?.length === 1;
const theOnlyToken = hasOnlyOneToken ? tokensList[0] : undefined;
const isOnlyTokenNotSelected =
theOnlyToken && theOnlyToken?.symbol !== selectedToken?.symbol;

Expand All @@ -198,17 +190,16 @@ export function TokenSelect({
return;
}
// when selected token is not supported, clear it
const supportedSymbols =
tokens?.flatMap((tok) => [tok.symbol, tok.symbolOnNetwork]) ?? [];
const supportedSymbols = tokensList?.flatMap((tok) => tok.symbol) ?? [];

if (
selectedToken &&
tokens?.[0] &&
tokensList?.[0] &&
!supportedSymbols.includes(selectedToken.symbol)
) {
onTokenChange(tokens[0]);
onTokenChange(tokensList[0]);
}
}, [bridgeTokensList, tokensList, onTokenChange, selectedToken]);
}, [tokensList, onTokenChange, selectedToken]);

const rowRenderer = useCallback(
({ key, index, style }) => {
Expand Down
Loading
Loading