Skip to content

Commit

Permalink
feat(bridge-ui-v2): Claim & Release (#14267)
Browse files Browse the repository at this point in the history
  • Loading branch information
jscriptcoder authored Jul 26, 2023
1 parent 723eb60 commit 6c6089e
Show file tree
Hide file tree
Showing 22 changed files with 555 additions and 51 deletions.
18 changes: 10 additions & 8 deletions packages/bridge-ui-v2/src/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
export const recommentProcessingFee = {
ethGasLimit: BigInt(900000),
erc20NotDeployedGasLimit: BigInt(3100000),
erc20DeployedGasLimit: BigInt(1100000),
ethGasLimit: BigInt(900_000),
erc20NotDeployedGasLimit: BigInt(3_100_000),
erc20DeployedGasLimit: BigInt(1_100_000),
};

export const processingFeeComponent = {
closingDelayOptionClick: 300,
intervalComputeRecommendedFee: 20000,
intervalComputeRecommendedFee: 20_000,
};

export const bridge = {
noOwnerGasLimit: BigInt(140000),
noTokenDeployedGasLimit: BigInt(3000000),
export const bridgeService = {
noOwnerGasLimit: BigInt(140_000),
noTokenDeployedGasLimit: BigInt(3_000_000),
erc20GasLimitThreshold: BigInt(2_500_000),
unpredictableGasLimit: BigInt(1_000_000),
};

export const pendingTransaction = {
waitTimeout: 300000,
waitTimeout: 300_000,
};
2 changes: 1 addition & 1 deletion packages/bridge-ui-v2/src/components/Bridge/Actions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
$: disableBridge = canDoNothing || $insufficientAllowance || $insufficientBalance || bridging;
// General loading state
$: loading = approving || bridging;
// $: loading = approving || bridging;
</script>

<div class="f-between-center w-full gap-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script lang="ts">
import { isAddress } from 'ethereum-address';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { Alert } from '$components/Alert';
import { classNames } from '$libs/util/classNames';
import { uid } from '$libs/util/uid';
let input: HTMLInputElement;
Expand All @@ -15,9 +14,10 @@
let isValidEthereumAddress = false;
let tooShort = true;
let classes = classNames('', $$props.class);
const dispatch = createEventDispatcher();
// TODO: nope!!, this should go inside a function whose arguments
// are the values that trigger reactivity
$: {
ethereumAddress;
if (ethereumAddress.length > 41) {
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge-ui-v2/src/components/Bridge/NFTBridge.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
let isAddressValid = false;
let contractAddress = '';
let imageUrls = Array<any>();
let imageUrls = Array<string>();
$: tokenIds = $tokenIdStore;
$: isButtonDisabled = !($tokenIdStore?.length ?? 0);
Expand All @@ -38,6 +38,7 @@
});
function resetAllStates() {
// TODO: change to $store format
isAddressValid = false;
contractTypeStore.set('');
tokenIdStore.set([]);
Expand All @@ -48,6 +49,7 @@
async function handleImport() {
contractTypeStore.set(await detectContractType(contractAddress, tokenIds[0]));
let result = null;
// TODO: use TokenType enum
if ($contractTypeStore === 'ERC721') {
result = await fetchERC721Images(contractAddress, tokenIds);
} else if ($contractTypeStore === 'ERC1155') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { Tooltip } from '$components/Tooltip';
Expand All @@ -10,6 +10,7 @@
const dispatch = createEventDispatcher();
// TODO: nope!!
$: {
let tokenIds;
if (tokenIdInput === '') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
let dialogId = `dialog-${uid()}`;
let modalOpen = false;
let invalidAddress = false;
let invalidAddress = false; // TODO: will be used soon
let prevRecipientAddress: Maybe<Address> = null;
let inputBox: InputBox;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
let classes = classNames('ChainSelector', $$props.class);
let buttonClasses = classNames(
'px-2 body-small-regular bg-neutral-background',
small ? 'py-[6px]' : 'py-[10px]',
'body-regular bg-neutral-background',
small ? 'px-2 py-[6px]' : 'px-6 py-[10px]',
small ? 'rounded-md' : 'rounded-[10px]',
small ? 'w-auto' : 'w-full',
);
Expand Down
115 changes: 115 additions & 0 deletions packages/bridge-ui-v2/src/libs/bridge/Bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { getContract } from '@wagmi/core';
import type { Hash } from 'viem';

import { bridgeABI } from '$abi';
import { chainContractsMap } from '$libs/chain';
import { MessageStatusError, WrongChainError, WrongOwnerError } from '$libs/error';
import type { BridgeProver } from '$libs/proof';
import { getLogger } from '$libs/util/logger';

import { type BridgeArgs, type ClaimArgs, MessageStatus, type ReleaseArgs } from './types';

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

export abstract class Bridge {
protected readonly _prover: BridgeProver;

constructor(prover: BridgeProver) {
this._prover = prover;
}

/**
* We are gonna run some common checks here:
* 1. Check that the wallet is connected to the destination chain
* 2. Check that the message is owned by the user
* 3. Check that the message has not been claimed already
* 4. Check that the message has not failed
*
* Important: wallet must be connected to the destination chain
*/
protected async beforeClaiming({ msgHash, message, wallet }: ClaimArgs) {
const destChainId = Number(message.destChainId);
// Are we connected to the destination chain?
if (wallet.chain.id !== destChainId) {
throw new WrongChainError('wallet must be connected to the destination chain');
}

const { owner } = message;
const userAddress = wallet.account.address;
// Are we the owner of the message?
if (owner.toLowerCase() !== userAddress.toLowerCase()) {
throw new WrongOwnerError('user cannot process this as it is not their message');
}

const destBridgeAddress = chainContractsMap[wallet.chain.id].bridgeAddress;

const destBridgeContract = getContract({
address: destBridgeAddress,
abi: bridgeABI,

// We are gonna resuse this contract to actually process the message
// so we'll need to sign the transaction
walletClient: wallet,
});

const messageStatus: MessageStatus = await destBridgeContract.read.getMessageStatus([msgHash]);

log(`Claiming message with status ${messageStatus}`);

// Has it been claimed already?
if (messageStatus === MessageStatus.DONE) {
throw new MessageStatusError('message already processed');
}

// Has it failed?
if (messageStatus === MessageStatus.FAILED) {
throw new MessageStatusError('user can not process this as message has failed');
}

return { messageStatus, destBridgeContract };
}

/**
* We are gonna run the following checks here:
* 1. Check that the wallet is connected to the source chain
* 2. Check that the message is owned by the user
* 3. Check that the message has failed
*/
protected async beforeReleasing({ msgHash, message, wallet }: ClaimArgs) {
const srcChainId = Number(message.srcChainId);
// Are we connected to the source chain?
if (wallet.chain.id !== srcChainId) {
throw new WrongChainError('wallet must be connected to the source chain');
}

const { owner } = message;
const userAddress = wallet.account.address;
// Are we the owner of the message?
if (owner.toLowerCase() !== userAddress.toLowerCase()) {
throw new WrongOwnerError('user cannot process this as it is not their message');
}

// Before releasing we need to make sure the message has failed
const destChainId = Number(message.destChainId);
const destBridgeAddress = chainContractsMap[destChainId].bridgeAddress;

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

const messageStatus: MessageStatus = await destBridgeContract.read.getMessageStatus([msgHash]);

log(`Releasing message with status ${messageStatus}`);

if (messageStatus !== MessageStatus.FAILED) {
throw new MessageStatusError('message must fail to release funds');
}
}

abstract estimateGas(args: BridgeArgs): Promise<bigint>;
abstract bridge(args: BridgeArgs): Promise<Hash>;
abstract claim(args: ClaimArgs): Promise<Hash>;
abstract release(args: ReleaseArgs): Promise<Hash>;
}
14 changes: 11 additions & 3 deletions packages/bridge-ui-v2/src/libs/bridge/ERC1155Bridge.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import type { Hex } from 'viem';
import type { Hash, Hex } from 'viem';

import type { Bridge } from './types';
import { Bridge } from './Bridge';

export class ERC1155Bridge implements Bridge {
export class ERC1155Bridge extends Bridge {
async estimateGas(): Promise<bigint> {
return Promise.resolve(BigInt(0));
}

async bridge(): Promise<Hex> {
return Promise.resolve('0x');
}

async claim() {
return Promise.resolve('0x' as Hash);
}

async release() {
return Promise.resolve('0x' as Hash);
}
}
86 changes: 80 additions & 6 deletions packages/bridge-ui-v2/src/libs/bridge/ERC20Bridge.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { getContract } from '@wagmi/core';
import { getContract, type Hash } from '@wagmi/core';
import { UserRejectedRequestError } from 'viem';

import { erc20ABI, tokenVaultABI } from '$abi';
import { bridge } from '$config';
import { bridgeService } from '$config';
import { chainContractsMap } from '$libs/chain';
import { ApproveError, InsufficientAllowanceError, NoAllowanceRequiredError, SendERC20Error } from '$libs/error';
import type { BridgeProver } from '$libs/proof';
import { getLogger } from '$libs/util/logger';

import type { ApproveArgs, Bridge, ERC20BridgeArgs, RequireAllowanceArgs, SendERC20Args } from './types';
import { Bridge } from './Bridge';
import {
type ApproveArgs,
type ClaimArgs,
type ERC20BridgeArgs,
MessageStatus,
type ReleaseArgs,
type RequireAllowanceArgs,
type SendERC20Args,
} from './types';

const log = getLogger('ERC20Bridge');

export class ERC20Bridge implements Bridge {
export class ERC20Bridge extends Bridge {
private static async _prepareTransaction(args: ERC20BridgeArgs) {
const {
to,
Expand All @@ -33,9 +44,9 @@ export class ERC20Bridge implements Bridge {
const refundAddress = wallet.account.address;

const gasLimit = !isTokenAlreadyDeployed
? BigInt(bridge.noTokenDeployedGasLimit)
? BigInt(bridgeService.noTokenDeployedGasLimit)
: processingFee > 0
? bridge.noOwnerGasLimit
? bridgeService.noOwnerGasLimit
: BigInt(0);

const sendERC20Args: SendERC20Args = [
Expand All @@ -54,6 +65,10 @@ export class ERC20Bridge implements Bridge {
return { tokenVaultContract, sendERC20Args };
}

constructor(prover: BridgeProver) {
super(prover);
}

async estimateGas(args: ERC20BridgeArgs) {
const { tokenVaultContract, sendERC20Args } = await ERC20Bridge._prepareTransaction(args);
const [, , , , , processingFee] = sendERC20Args;
Expand Down Expand Up @@ -162,4 +177,63 @@ export class ERC20Bridge implements Bridge {
throw new SendERC20Error('failed to bridge ERC20 token', { cause: err });
}
}

async claim(args: ClaimArgs) {
const { messageStatus, destBridgeContract } = await super.beforeClaiming(args);

let txHash: Hash;
const { msgHash, message } = args;
const srcChainId = Number(message.srcChainId);
const destChainId = Number(message.destChainId);

if (messageStatus === MessageStatus.NEW) {
const proof = await this._prover.generateProofToProcessMessage(msgHash, srcChainId, destChainId);

if (message.gasLimit > bridgeService.erc20GasLimitThreshold) {
txHash = await destBridgeContract.write.processMessage([message, proof], {
gas: message.gasLimit,
});
} else {
txHash = await destBridgeContract.write.processMessage([message, proof]);
}

log('Transaction hash for processMessage call', txHash);

// TODO: handle unpredictable gas limit error
// by trying with a higher gas limit
} else {
// MessageStatus.RETRIABLE
log('Retrying message', message);

// Last attempt to send the message: isLastAttempt = true
txHash = await destBridgeContract.write.retryMessage([message, true]);

log('Transaction hash for retryMessage call', txHash);
}

return txHash;
}

async release(args: ReleaseArgs) {
await super.beforeReleasing(args);

const { msgHash, message, wallet } = args;
const srcChainId = Number(message.srcChainId);
const destChainId = Number(message.destChainId);

const proof = await this._prover.generateProofToRelease(msgHash, srcChainId, destChainId);

const srcTokenVaultAddress = chainContractsMap[wallet.chain.id].tokenVaultAddress;
const srcTokenVaultContract = getContract({
walletClient: wallet,
abi: tokenVaultABI,
address: srcTokenVaultAddress,
});

const txHash = await srcTokenVaultContract.write.releaseERC20([message, proof]);

log('Transaction hash for releaseEther call', txHash);

return txHash;
}
}
Loading

0 comments on commit 6c6089e

Please sign in to comment.