Skip to content

Commit 7f899d0

Browse files
feat: add native wsteth support (#246)
Co-authored-by: Jack Hamer <[email protected]>
1 parent 5eef311 commit 7f899d0

File tree

17 files changed

+805
-91
lines changed

17 files changed

+805
-91
lines changed

components/common/input/InputLine.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ const inputted = computed({
2727
<style lang="scss" scoped>
2828
.input-line {
2929
@apply block w-full overflow-hidden rounded-none border-b-2 bg-transparent leading-snug outline-none transition placeholder:text-neutral-300 dark:placeholder:text-neutral-700;
30-
@apply border-neutral-200 hover:border-neutral-300 focus:border-neutral-950;
31-
@apply dark:border-neutral-800 dark:hover:border-neutral-600 dark:focus:border-white;
30+
@apply border-neutral-200 dark:border-neutral-800;
31+
&:not(.has-error) {
32+
@apply hover:border-neutral-300 focus:border-neutral-950;
33+
@apply dark:hover:border-neutral-600 dark:focus:border-white;
34+
}
3235
&.has-error {
3336
@apply border-error-300;
3437
}

components/token/TokenSelectModal.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<CommonLineButtonsGroup class="category" :gap="false" :margin-y="false">
2828
<TokenLine
2929
v-for="item in displayedTokens"
30-
:key="item.address"
30+
:key="item.l2Address ? `${item.address}-${item.l2Address}` : item.address"
3131
class="token-line"
3232
v-bind="item"
3333
@click="selectedToken = item"
@@ -43,7 +43,7 @@
4343
<TokenBalance
4444
v-for="item in group.balances"
4545
v-bind="item"
46-
:key="item.address"
46+
:key="item.l2Address ? `${item.address}-${item.l2Address}` : item.address"
4747
variant="light"
4848
@click="selectedToken = item"
4949
/>
@@ -137,7 +137,11 @@ const selectedToken = computed({
137137
},
138138
set: (value) => {
139139
if (value) {
140-
selectedTokenAddress.value = value.address;
140+
// Handle special case for L1 tokens with multiple L2 counterparts (native and bridged) - create unique identifier
141+
const hasMultipleTokens =
142+
props.tokens.filter((e) => e.address === value.address).length > 1 ||
143+
props.balances.filter((e) => e.address === value.address).length > 1;
144+
selectedTokenAddress.value = hasMultipleTokens ? `${value.address}-${value.l2Address}` : value.address;
141145
} else {
142146
selectedTokenAddress.value = undefined;
143147
}

composables/transaction/useAllowance.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export default (
108108
error: err as Error,
109109
parentFunctionName: "executeSetAllowance",
110110
parentFunctionParams: [],
111-
accountAddress: accountAddress.value || "",
112111
filePath: "composables/transaction/useAllowance.ts",
113112
});
114113
throw err;

composables/zksync/deposit/useTransaction.ts

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,109 @@
1+
import { readContract, writeContract } from "@wagmi/core";
2+
import { zeroAddress, type Address, type Hash } from "viem";
3+
import { L1Signer } from "zksync-ethers";
4+
import { getERC20DefaultBridgeData, REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT } from "zksync-ethers/build/utils";
5+
16
import { useSentryLogger } from "@/composables/useSentryLogger";
7+
import { L1_BRIDGE_ABI } from "@/data/abis/l1BridgeAbi";
8+
import { wagmiConfig } from "@/data/wagmi";
29

310
import type { DepositFeeValues } from "@/composables/zksync/deposit/useFee";
411
import type { BigNumberish } from "ethers";
5-
import type { L1Signer } from "zksync-ethers";
612

713
export default (getL1Signer: () => Promise<L1Signer | undefined>) => {
814
const status = ref<"not-started" | "processing" | "waiting-for-signature" | "done">("not-started");
915
const error = ref<Error | undefined>();
10-
const ethTransactionHash = ref<string | undefined>();
16+
const ethTransactionHash = ref<Hash | undefined>();
1117
const eraWalletStore = useZkSyncWalletStore();
1218
const { captureException } = useSentryLogger();
1319

1420
const { validateAddress } = useScreening();
1521

22+
const handleCustomBridgeDeposit = async (
23+
transaction: {
24+
to: Address;
25+
tokenAddress: Address;
26+
amount: BigNumberish;
27+
bridgeAddress: Address;
28+
gasPerPubdata?: bigint;
29+
l2GasLimit?: bigint;
30+
refundRecipient?: Address;
31+
},
32+
fee: DepositFeeValues
33+
) => {
34+
const l1Signer = await getL1Signer();
35+
if (!l1Signer) throw new Error("L1 signer is not available");
36+
37+
const l2BridgeAddress = await readContract(wagmiConfig, {
38+
address: transaction.bridgeAddress as Address,
39+
abi: L1_BRIDGE_ABI,
40+
functionName: "l2Bridge",
41+
});
42+
const bridgeData = await getERC20DefaultBridgeData(transaction.tokenAddress, l1Signer.provider);
43+
44+
const gasPerPubdata = transaction.gasPerPubdata ?? BigInt(REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT);
45+
const l2Value = 0n; // L2 value is not used in this context
46+
const l2GasLimit = await l1Signer.providerL2.estimateCustomBridgeDepositL2Gas(
47+
transaction.bridgeAddress,
48+
l2BridgeAddress,
49+
transaction.tokenAddress,
50+
transaction.amount.toString(),
51+
transaction.to,
52+
bridgeData,
53+
l1Signer.address,
54+
gasPerPubdata,
55+
l2Value
56+
);
57+
58+
const baseCost = await l1Signer.getBaseCost({
59+
gasLimit: l2GasLimit,
60+
gasPerPubdataByte: gasPerPubdata,
61+
});
62+
63+
const overrides = {
64+
gasPrice: fee.gasPrice,
65+
gasLimit: fee.l1GasLimit,
66+
maxFeePerGas: fee.maxFeePerGas,
67+
maxPriorityFeePerGas: fee.maxPriorityFeePerGas,
68+
};
69+
if (overrides.gasPrice && overrides.maxFeePerGas) {
70+
overrides.gasPrice = undefined;
71+
}
72+
73+
const hash = await writeContract(wagmiConfig, {
74+
address: transaction.bridgeAddress as Address,
75+
abi: L1_BRIDGE_ABI,
76+
functionName: "deposit",
77+
args: [
78+
transaction.to,
79+
transaction.tokenAddress,
80+
BigInt(transaction.amount.toString()),
81+
transaction.l2GasLimit ?? 400000n,
82+
gasPerPubdata,
83+
transaction.refundRecipient ?? zeroAddress,
84+
],
85+
value: baseCost + (overrides.maxPriorityFeePerGas ? BigInt(overrides.maxPriorityFeePerGas) : 0n),
86+
});
87+
88+
return {
89+
from: l1Signer.address,
90+
to: transaction.to,
91+
hash,
92+
// eslint-disable-next-line require-await
93+
wait: async () => ({
94+
from: l1Signer.address,
95+
to: transaction.to,
96+
hash,
97+
}),
98+
};
99+
};
100+
16101
const commitTransaction = async (
17102
transaction: {
18-
to: string;
19-
tokenAddress: string;
103+
to: Address;
104+
tokenAddress: Address;
20105
amount: BigNumberish;
106+
bridgeAddress?: Address;
21107
},
22108
fee: DepositFeeValues
23109
) => {
@@ -42,18 +128,29 @@ export default (getL1Signer: () => Promise<L1Signer | undefined>) => {
42128
}
43129

44130
status.value = "waiting-for-signature";
45-
const depositResponse = await wallet.deposit({
46-
to: transaction.to,
47-
token: transaction.tokenAddress,
48-
amount: transaction.amount,
49-
l2GasLimit: fee.l2GasLimit,
50-
approveBaseERC20: true,
51-
overrides,
52-
});
53131

54-
ethTransactionHash.value = depositResponse.hash;
55-
status.value = "done";
56-
return depositResponse;
132+
if (transaction.bridgeAddress) {
133+
const depositResponse = await handleCustomBridgeDeposit(
134+
{ ...transaction, bridgeAddress: transaction.bridgeAddress },
135+
fee
136+
);
137+
ethTransactionHash.value = depositResponse.hash;
138+
status.value = "done";
139+
return depositResponse;
140+
} else {
141+
const depositResponse = await wallet.deposit({
142+
to: transaction.to,
143+
token: transaction.tokenAddress,
144+
amount: transaction.amount,
145+
l2GasLimit: fee.l2GasLimit,
146+
approveBaseERC20: true,
147+
overrides,
148+
});
149+
150+
ethTransactionHash.value = depositResponse.hash as Hash;
151+
status.value = "done";
152+
return depositResponse;
153+
}
57154
} catch (err) {
58155
error.value = formatError(err as Error);
59156
status.value = "not-started";

composables/zksync/useFee.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { type Provider } from "zksync-ethers";
1+
import { EIP712_TX_TYPE } from "zksync-ethers/build/utils";
22

33
import type { Token, TokenAmount } from "@/types";
4+
import type { BigNumberish, ethers } from "ethers";
5+
import type { Provider } from "zksync-ethers";
6+
import type { Address, PaymasterParams } from "zksync-ethers/build/types";
47

58
export type FeeEstimationParams = {
69
type: "transfer" | "withdrawal";
@@ -33,12 +36,48 @@ export default (
3336
}
3437
const feeTokenBalance = balances.value.find((e) => e.address === feeToken.value!.address);
3538
if (!feeTokenBalance) return true;
36-
if (totalFee.value && BigInt(totalFee.value) > feeTokenBalance.amount) {
39+
if (totalFee.value && BigInt(totalFee.value) > BigInt(feeTokenBalance.amount)) {
3740
return false;
3841
}
3942
return true;
4043
});
4144

45+
// We need to calculate gas limit with custom function since the new version of the SDK fails
46+
const getCustomGasLimit = async (transaction: {
47+
token: Address;
48+
amount: BigNumberish;
49+
from?: Address;
50+
to?: Address;
51+
bridgeAddress?: Address;
52+
paymasterParams?: PaymasterParams;
53+
overrides?: ethers.Overrides;
54+
}): Promise<bigint> => {
55+
const { ...tx } = transaction;
56+
if ((tx.to === null || tx.to === undefined) && (tx.from === null || tx.from === undefined)) {
57+
throw new Error("Withdrawal target address is undefined!");
58+
}
59+
tx.to ??= tx.from;
60+
tx.overrides ??= {};
61+
tx.overrides.from ??= tx.from;
62+
tx.overrides.type ??= EIP712_TX_TYPE;
63+
64+
const provider = getProvider();
65+
const bridge = await provider.connectL2Bridge(tx.bridgeAddress!);
66+
let populatedTx = await bridge.withdraw.populateTransaction(tx.to!, tx.token, tx.amount, tx.overrides);
67+
if (tx.paymasterParams) {
68+
populatedTx = {
69+
...populatedTx,
70+
customData: {
71+
paymasterParams: tx.paymasterParams,
72+
},
73+
};
74+
}
75+
76+
const gasLimit = await provider.estimateGas(populatedTx);
77+
78+
return gasLimit;
79+
};
80+
4281
const {
4382
inProgress,
4483
error,
@@ -50,17 +89,31 @@ export default (
5089

5190
const provider = getProvider();
5291
const tokenBalance = balances.value.find((e) => e.address === params!.tokenAddress)?.amount || "1";
92+
const token = balances.value.find((e) => e.address === params!.tokenAddress);
93+
const isCustomBridgeToken = !!token?.l2BridgeAddress;
94+
5395
const [price, limit] = await Promise.all([
5496
retry(() => provider.getGasPrice()),
5597
retry(() => {
56-
return provider[params!.type === "transfer" ? "estimateGasTransfer" : "estimateGasWithdraw"]({
57-
from: params!.from,
58-
to: params!.to,
59-
token: params!.tokenAddress,
60-
amount: tokenBalance,
61-
});
98+
if (isCustomBridgeToken) {
99+
return getCustomGasLimit({
100+
from: params!.from,
101+
to: params!.to,
102+
token: params!.tokenAddress,
103+
amount: tokenBalance,
104+
bridgeAddress: token?.l2BridgeAddress,
105+
});
106+
} else {
107+
return provider[params!.type === "transfer" ? "estimateGasTransfer" : "estimateGasWithdraw"]({
108+
from: params!.from,
109+
to: params!.to,
110+
token: params!.tokenAddress,
111+
amount: tokenBalance,
112+
});
113+
}
62114
}),
63115
]);
116+
64117
gasPrice.value = price;
65118
gasLimit.value = limit;
66119
},

0 commit comments

Comments
 (0)