Skip to content

Commit

Permalink
Merge branch 'main' into av/feat/qr-code-modal
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonioVentilii-DFINITY authored and AntonioVentilii committed May 24, 2024
2 parents 998cdfd + 41826e1 commit b0b94bb
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 51 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dfinity/oisy-wallet",
"version": "0.0.16",
"version": "0.0.18",
"private": true,
"license": "Apache-2.0",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.78.0"
channel = "1.75.0"
targets = ["wasm32-unknown-unknown"]
17 changes: 4 additions & 13 deletions src/frontend/src/icp/components/fee/EthereumEstimatedFee.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,19 @@
} from '$icp/stores/ethereum-fee.store';
import { isTokenCkErc20Ledger } from '$icp/utils/ic-send.utils';
import { token } from '$lib/derived/token.derived';
import type { IcCkToken, IcToken } from '$icp/types/ic';
import { icrcTokens } from '$icp/derived/icrc.derived';
import type { LedgerCanisterIdText } from '$icp/types/canister';
import type { IcToken } from '$icp/types/ic';
import { ethereumFeeTokenCkEth } from '$icp/derived/ethereum-fee.derived';
let ckEr20 = false;
$: ckEr20 = isTokenCkErc20Ledger($token as IcToken);
let feeLedgerCanisterId: LedgerCanisterIdText | undefined;
$: feeLedgerCanisterId = ($token as IcCkToken).feeLedgerCanisterId;
let tokenCkEth: IcToken | undefined;
$: tokenCkEth = nonNullish(feeLedgerCanisterId)
? $icrcTokens.find(({ ledgerCanisterId }) => ledgerCanisterId === feeLedgerCanisterId)
: undefined;
const { store } = getContext<EthereumFeeContext>(ETHEREUM_FEE_CONTEXT_KEY);
let feeSymbol: string;
$: feeSymbol = ckEr20
? tokenCkEth?.symbol ?? $ckEthereumNativeToken.symbol
? $ethereumFeeTokenCkEth?.symbol ?? $ckEthereumNativeToken.symbol
: $ckEthereumNativeToken.symbol;
const { store } = getContext<EthereumFeeContext>(ETHEREUM_FEE_CONTEXT_KEY);
let maxTransactionFee: bigint | undefined | null = undefined;
$: maxTransactionFee = $store?.maxTransactionFee;
</script>
Expand Down
64 changes: 54 additions & 10 deletions src/frontend/src/icp/components/send/IcSendAmount.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,26 @@
import { assertCkBTCUserInputAmount } from '$icp/utils/ckbtc.utils';
import { IcAmountAssertionError } from '$icp/types/ic-send';
import { ckBtcMinterInfoStore } from '$icp/stores/ckbtc.store';
import { assertCkETHMinFee, assertCkETHMinWithdrawalAmount } from '$icp/utils/cketh.utils';
import {
assertCkETHBalanceEstimatedFee,
assertCkETHMinFee,
assertCkETHMinWithdrawalAmount
} from '$icp/utils/cketh.utils';
import { isNetworkIdEthereum } from '$lib/utils/network.utils';
import { isNetworkIdBTC } from '$icp/utils/ic-send.utils';
import { i18n } from '$lib/stores/i18n.store';
import { ckEthMinterInfoStore } from '$icp-eth/stores/cketh.store';
import { tokenCkEthLedger } from '$icp/derived/ic-token.derived';
import { tokenCkErc20Ledger, tokenCkEthLedger } from '$icp/derived/ic-token.derived';
import { ckEthereumNativeTokenId } from '$icp-eth/derived/cketh.derived';
import SendInputAmount from '$lib/components/send/SendInputAmount.svelte';
import { getMaxTransactionAmount } from '$lib/utils/token.utils';
import { getContext } from 'svelte';
import {
ETHEREUM_FEE_CONTEXT_KEY,
type EthereumFeeContext
} from '$icp/stores/ethereum-fee.store';
import { balancesStore } from '$lib/stores/balances.store';
import { ethereumFeeTokenCkEth } from '$icp/derived/ethereum-fee.derived';
export let amount: number | undefined = undefined;
export let amountError: IcAmountAssertionError | undefined;
Expand All @@ -31,13 +42,15 @@
let fee: bigint | undefined;
$: fee = ($token as IcToken).fee;
const { store: ethereumFeeStore } = getContext<EthereumFeeContext>(ETHEREUM_FEE_CONTEXT_KEY);
$: customValidate = (userAmount: BigNumber): Error | undefined => {
if (isNullish(fee)) {
return;
}
if (isNetworkIdBTC(networkId)) {
let error = assertCkBTCUserInputAmount({
const error = assertCkBTCUserInputAmount({
amount: userAmount,
minterInfo: $ckBtcMinterInfoStore?.[$tokenId],
tokenDecimals: $tokenDecimals,
Expand All @@ -49,8 +62,9 @@
}
}
// if CkEth, asset the minimal withdrawal amount is met and the amount should at least be bigger than the fee.
if (isNetworkIdEthereum(networkId) && $tokenCkEthLedger) {
let error = assertCkETHMinWithdrawalAmount({
const error = assertCkETHMinWithdrawalAmount({
amount: userAmount,
tokenDecimals: $tokenDecimals,
tokenSymbol: $tokenSymbol,
Expand All @@ -61,9 +75,7 @@
if (nonNullish(error)) {
return error;
}
}
if (isNetworkIdEthereum(networkId)) {
return assertCkETHMinFee({
amount: userAmount,
tokenSymbol: $tokenSymbol,
Expand All @@ -72,13 +84,41 @@
});
}
const total = userAmount.add(fee);
const assertBalance = (): IcAmountAssertionError | undefined => {
const total = userAmount.add(fee ?? BigNumber.from(0n));
if (total.gt($balance ?? BigNumber.from(0n))) {
return new IcAmountAssertionError($i18n.send.assertion.insufficient_funds);
}
return undefined;
};
// if CkErc20, the entered amount should be covered by fee + balance and the ckEth balance should cover the estimated fee.
if (isNetworkIdEthereum(networkId) && $tokenCkErc20Ledger) {
const error = assertBalance();
if (nonNullish(error)) {
return error;
}
if (total.gt($balance ?? BigNumber.from(0n))) {
return new IcAmountAssertionError($i18n.send.assertion.insufficient_funds);
const errorEstimatedFee = assertCkETHBalanceEstimatedFee({
balance: nonNullish($ethereumFeeTokenCkEth)
? $balancesStore?.[$ethereumFeeTokenCkEth.id]?.data
: undefined,
tokenCkEth: $ethereumFeeTokenCkEth,
feeStoreData: $ethereumFeeStore,
i18n: $i18n
});
if (nonNullish(errorEstimatedFee)) {
return errorEstimatedFee;
}
return undefined;
}
return;
return assertBalance();
};
$: calculateMax = (): number => {
Expand All @@ -89,10 +129,14 @@
tokenStandard: $tokenStandard
});
};
let sendInputAmount: SendInputAmount | undefined;
$: $ethereumFeeStore, (() => sendInputAmount?.triggerValidate())();
</script>

<SendInputAmount
bind:amount
bind:this={sendInputAmount}
tokenDecimals={$tokenDecimals}
{customValidate}
{calculateMax}
Expand Down
6 changes: 5 additions & 1 deletion src/frontend/src/icp/components/send/IcSendModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,17 @@
});
/**
* Init bitcoin and Ethereum fee context stores
* Bitcoin fee context store
*/
setContext<BitcoinFeeContextType>(BITCOIN_FEE_CONTEXT_KEY, {
store: initBitcoinFeeStore()
});
/**
* Ethereum fee context store
*/
setContext<EthereumFeeContextType>(ETHEREUM_FEE_CONTEXT_KEY, {
store: initEthereumFeeStore()
});
Expand Down
18 changes: 18 additions & 0 deletions src/frontend/src/icp/derived/ethereum-fee.derived.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { icrcTokens } from '$icp/derived/icrc.derived';
import type { IcCkToken, IcToken } from '$icp/types/ic';
import { token } from '$lib/derived/token.derived';
import { nonNullish } from '@dfinity/utils';
import { derived, type Readable } from 'svelte/store';

/**
* Ethereum fees for converting ckErc20 in ckEth are paid in ckEth.
*/
export const ethereumFeeTokenCkEth: Readable<IcToken | undefined> = derived(
[token, icrcTokens],
([$token, $icrcTokens]) => {
const feeLedgerCanisterId = ($token as IcCkToken).feeLedgerCanisterId;
return nonNullish(feeLedgerCanisterId)
? $icrcTokens.find(({ ledgerCanisterId }) => ledgerCanisterId === feeLedgerCanisterId)
: undefined;
}
);
4 changes: 2 additions & 2 deletions src/frontend/src/icp/services/ic-send.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const sendIcrc = async ({

// UI validates addresses and disable form if not compliant. Therefore, this issue should unlikely happen.
if (!validIcrcAddress) {
throw new Error(get(i18n).send.error.invalid_address);
throw new Error(get(i18n).send.error.invalid_destination);
}

progress(SendIcStep.SEND);
Expand All @@ -125,7 +125,7 @@ const sendIcp = async ({

// UI validates addresses and disable form if not compliant. Therefore, this issue should unlikely happen.
if (!validIcrcAddress && !validIcpAddress) {
throw new Error(get(i18n).send.error.invalid_address);
throw new Error(get(i18n).send.error.invalid_destination);
}

progress(SendIcStep.SEND);
Expand Down
44 changes: 44 additions & 0 deletions src/frontend/src/icp/utils/cketh.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { CkEthMinterInfoData } from '$icp-eth/stores/cketh.store';
import type { EthereumFeeStoreData } from '$icp/stores/ethereum-fee.store';
import type { IcToken } from '$icp/types/ic';
import { IcAmountAssertionError } from '$icp/types/ic-send';
import { formatToken } from '$lib/utils/format.utils';
import { replacePlaceholders } from '$lib/utils/i18n.utils';
Expand Down Expand Up @@ -83,3 +85,45 @@ export const assertCkETHMinFee = ({

return undefined;
};

export const assertCkETHBalanceEstimatedFee = ({
balance,
tokenCkEth,
feeStoreData,
i18n
}: {
balance: BigNumber | undefined | null;
tokenCkEth: IcToken | undefined;
feeStoreData: EthereumFeeStoreData;
i18n: I18n;
}): IcAmountAssertionError | undefined => {
const ethBalance = balance ?? BigNumber.from(0n);

// We skip validation checks here for zero balance because it makes the UI/UX ungraceful if the balance is just not yet loaded.
if (ethBalance.isZero()) {
return undefined;
}

if (isNullish(tokenCkEth)) {
return new IcAmountAssertionError(i18n.send.assertion.unknown_cketh);
}

const { decimals, symbol } = tokenCkEth;

const estimatedFee = BigNumber.from(feeStoreData?.maxTransactionFee ?? 0n);

if (estimatedFee.gt(ethBalance)) {
return new IcAmountAssertionError(
replacePlaceholders(i18n.send.assertion.minimum_cketh_balance, {
$amount: formatToken({
value: estimatedFee,
unitName: decimals,
displayDecimals: decimals
}),
$symbol: symbol
})
);
}

return undefined;
};
21 changes: 12 additions & 9 deletions src/frontend/src/lib/components/send/QRCodeModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
export let steps: WizardSteps;
export let currentStep: WizardStep | undefined = undefined;
const STEP_QRCODE = 'QRCode';
export let qrCodeStep: string;
let stepsPlusQr: WizardSteps;
$: stepsPlusQr = [
...steps,
{
name: STEP_QRCODE,
name: qrCodeStep,
title: $i18n.send.text.scan_qr
}
];
Expand All @@ -29,20 +28,24 @@
const goToStep = (stepName: string) => {
const stepNumber = stepsPlusQr.findIndex(({ name }) => name === stepName);
if (stepNumber === -1) {
modal.set(0);
return;
}
modal.set(stepNumber);
};
let resolveQrCodePromise:
| (({ status, code }: { status: QrStatus; code?: string }) => void)
| undefined = undefined;
export const scanQrCode = async ({
expectedToken
}: {
export const scanQrCode = ({
expectedToken
}: {
expectedToken: Token;
}): Promise<QrResponse> => {
const prevStep = currentStep;
goToStep(STEP_QRCODE);
goToStep(qrCodeStep);
return new Promise<{ status: QrStatus; code?: string | undefined }>((resolve) => {
resolveQrCodePromise = resolve;
Expand All @@ -52,7 +55,7 @@
toastsError({
msg: { text: $i18n.send.error.incompatible_token }
});
return Promise.reject(new Error('Token incompatible'));
return Promise.reject(new Error($i18n.send.error.incompatible_token));
}
return decodeQrCode({ status, code, expectedToken });
})
Expand All @@ -79,7 +82,7 @@
disablePointerEvents={currentStep?.name === 'Sending'}
>
<svelte:fragment slot="title">{$i18n.send.text.scan_qr}</svelte:fragment>
{#if currentStep?.name === STEP_QRCODE}
{#if currentStep?.name === qrCodeStep}
<QRCodeReaderModal on:nnsCancel={onCancel} on:nnsQRCode={onQRCode} />
{/if}
</WizardModal>
2 changes: 2 additions & 0 deletions src/frontend/src/lib/components/send/SendInputAmount.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
const debounceValidate = debounce(validate, 300);
$: amount, tokenDecimals, debounceValidate();
export const triggerValidate = debounceValidate;
</script>

<label for="amount" class="font-bold px-4.5">{$i18n.core.text.amount}</label>
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@
"minimum_ckbtc_amount": "The amount falls below the minimum of $amount ckBTC required for converting to BTC.",
"minimum_cketh_amount": "The amount falls below the minimum withdrawal amount of $amount $symbol.",
"minimum_ledger_fees": "The amount falls below the ledger fees $symbol.",
"minimum_cketh_balance": "The $symbol balance falls below the estimated fee of $amount.",
"unknown_cketh": "The ckETH token cannot be retrieved.",
"destination_address_invalid": "Destination address is invalid.",
"amount_invalid": "Amount is invalid.",
"insufficient_funds_for_gas": "Insufficient funds for gas",
Expand All @@ -241,7 +243,7 @@
"erc20_data_undefined": "Erc20 transaction Data cannot be undefined or null.",
"data_undefined": "Transaction Data cannot be undefined or null.",
"no_identity_calculate_fee": "No identity provided to calculate the fee for its principal.",
"invalid_address": "The address is invalid. Please try again with a valid address identifier.",
"invalid_destination": "The destination is invalid. Please try again with a valid wallet address or destination.",
"incompatible_token": "The token is incompatible. Please try again with a compatible token."
}
},
Expand Down
Loading

0 comments on commit b0b94bb

Please sign in to comment.