Skip to content

Commit

Permalink
feat(frontend): Swap feature
Browse files Browse the repository at this point in the history
  • Loading branch information
DenysKarmazynDFINITY committed Jan 10, 2025
1 parent 9c71891 commit b6ef3e1
Show file tree
Hide file tree
Showing 52 changed files with 1,773 additions and 68 deletions.
18 changes: 13 additions & 5 deletions src/frontend/src/icp/services/ic-send.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,23 @@ const send = async ({
});
};

const sendIcrc = ({
export const sendIcrc = ({
to,
amount,
identity,
ledgerCanisterId,
progress
}: IcTransferParams & Pick<IcToken, 'ledgerCanisterId'>): Promise<IcrcBlockIndex> => {
}: Omit<IcTransferParams, 'progress'> &
Partial<Pick<IcTransferParams, 'progress'>> &
Pick<IcToken, 'ledgerCanisterId'>): Promise<IcrcBlockIndex> => {
const validIcrcAddress = !invalidIcrcAddress(to);

// 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_destination);
}

progress(ProgressStepsSendIc.SEND);
progress?.(ProgressStepsSendIc.SEND);

return transferIcrc({
identity,
Expand All @@ -117,7 +119,13 @@ const sendIcrc = ({
});
};

const sendIcp = ({ to, amount, identity, progress }: IcTransferParams): Promise<BlockHeight> => {
export const sendIcp = ({
to,
amount,
identity,
progress
}: Omit<IcTransferParams, 'progress'> &
Partial<Pick<IcTransferParams, 'progress'>>): Promise<BlockHeight> => {
const validIcrcAddress = !invalidIcrcAddress(to);
const validIcpAddress = !invalidIcpAddress(to);

Expand All @@ -126,7 +134,7 @@ const sendIcp = ({ to, amount, identity, progress }: IcTransferParams): Promise<
throw new Error(get(i18n).send.error.invalid_destination);
}

progress(ProgressStepsSendIc.SEND);
progress?.(ProgressStepsSendIc.SEND);

return validIcrcAddress
? icrc1TransferIcp({
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/lib/canisters/kong_backend.canister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class KongBackendCanister extends Canister<KongBackendService> {
sendAmount,
referredBy,
receiveAmount,
destinationAddress,
receiveAddress,
sourceToken,
payTransactionId
}: KongSwapParams): Promise<bigint> => {
Expand All @@ -66,7 +66,7 @@ export class KongBackendCanister extends Canister<KongBackendService> {
receive_token: destinationToken.symbol,
pay_amount: sendAmount,
max_slippage: toNullable(maxSlippage),
receive_address: toNullable(destinationAddress),
receive_address: toNullable(receiveAddress),
receive_amount: toNullable(receiveAmount),
pay_tx_id: toNullable(payTransactionId),
referred_by: toNullable(referredBy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type { Token } from '$lib/types/token';
export let token: Token;
export let size: Extract<LogoSize, 'md' | 'xs'> = 'xs';
export let size: Extract<LogoSize, 'md' | 'xxs'> = 'xxs';
</script>

<div class="flex items-center">
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/lib/components/hero/Actions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Buy from '$lib/components/buy/Buy.svelte';
import Receive from '$lib/components/receive/Receive.svelte';
import Send from '$lib/components/send/Send.svelte';
import Swap from '$lib/components/swap/Swap.svelte';
import HeroButtonGroup from '$lib/components/ui/HeroButtonGroup.svelte';
import { allBalancesZero } from '$lib/derived/balances.derived';
import {
Expand Down Expand Up @@ -45,6 +46,9 @@
let isTransactionsPage = false;
$: isTransactionsPage = isRouteTransactions($page);
let swapAction = false;
$: swapAction = !isTransactionsPage;
let sendAction = true;
$: sendAction = !$allBalancesZero || isTransactionsPage;
</script>
Expand All @@ -67,6 +71,10 @@
<Send {isTransactionsPage} />
{/if}

{#if swapAction}
<Swap />
{/if}

{#if isTransactionsPage}
{#if convertEth}
{#if $networkICP}
Expand Down
43 changes: 9 additions & 34 deletions src/frontend/src/lib/components/manage/ManageTokens.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script lang="ts">
import { IconClose } from '@dfinity/gix-components';
import { debounce, nonNullish } from '@dfinity/utils';
import { createEventDispatcher, onMount } from 'svelte';
import { fade } from 'svelte/transition';
Expand All @@ -8,18 +7,16 @@
import type { Erc20UserToken } from '$eth/types/erc20-user-token';
import { icTokenErc20UserToken, icTokenEthereumUserToken } from '$eth/utils/erc20.utils';
import IcManageTokenToggle from '$icp/components/tokens/IcManageTokenToggle.svelte';
import type { IcCkToken } from '$icp/types/ic-token';
import type { IcrcCustomToken } from '$icp/types/icrc-custom-token';
import { icTokenIcrcCustomToken } from '$icp/utils/icrc.utils';
import IconSearch from '$lib/components/icons/IconSearch.svelte';
import ManageTokenToggle from '$lib/components/tokens/ManageTokenToggle.svelte';
import TokenLogo from '$lib/components/tokens/TokenLogo.svelte';
import TokenName from '$lib/components/tokens/TokenName.svelte';
import Button from '$lib/components/ui/Button.svelte';
import ButtonCancel from '$lib/components/ui/ButtonCancel.svelte';
import ButtonGroup from '$lib/components/ui/ButtonGroup.svelte';
import Card from '$lib/components/ui/Card.svelte';
import InputTextWithAction from '$lib/components/ui/InputTextWithAction.svelte';
import InputSearch from '$lib/components/ui/InputSearch.svelte';
import { allTokens } from '$lib/derived/all-tokens.derived';
import { exchanges } from '$lib/derived/exchange.derived';
import { pseudoNetworkChainFusion, selectedNetwork } from '$lib/derived/network.derived';
Expand All @@ -29,8 +26,8 @@
import type { Token } from '$lib/types/token';
import type { TokenToggleable } from '$lib/types/token-toggleable';
import { replacePlaceholders } from '$lib/utils/i18n.utils';
import { isNullishOrEmpty } from '$lib/utils/input.utils';
import { filterTokensForSelectedNetwork } from '$lib/utils/network.utils';
import { filterTokens } from '$lib/utils/token.utils';
import { pinEnabledTokensAtTop, sortTokens } from '$lib/utils/tokens.utils';
import SolManageTokenToggle from '$sol/components/tokens/SolManageTokenToggle.svelte';
import { isSolanaToken } from '$sol/utils/token.utils';
Expand Down Expand Up @@ -64,26 +61,15 @@
)
: [];
let filterTokens = '';
const updateFilter = () => (filterTokens = filter);
let tokensFilter = '';
const updateFilter = () => (tokensFilter = filter);
const debounceUpdateFilter = debounce(updateFilter);
let filter = '';
$: filter, debounceUpdateFilter();
const matchingToken = (token: Token): boolean =>
token.name.toLowerCase().includes(filterTokens.toLowerCase()) ||
token.symbol.toLowerCase().includes(filterTokens.toLowerCase()) ||
(icTokenIcrcCustomToken(token) &&
(token.alternativeName ?? '').toLowerCase().includes(filterTokens.toLowerCase()));
let filteredTokens: Token[] = [];
$: filteredTokens = isNullishOrEmpty(filterTokens)
? allTokensSorted
: allTokensSorted.filter((token) => {
const twinToken = (token as IcCkToken).twinToken;
return matchingToken(token) || (nonNullish(twinToken) && matchingToken(twinToken));
});
$: filteredTokens = filterTokens({ tokens: allTokensSorted, filter: tokensFilter });
let tokens: Token[] = [];
$: tokens = filteredTokens.map((token) => {
Expand Down Expand Up @@ -145,22 +131,11 @@
</script>

<div class="mb-4">
<InputTextWithAction
name="filter"
required={false}
bind:value={filter}
<InputSearch
bind:filter
noMatch={noTokensMatch}
placeholder={$i18n.tokens.placeholder.search_token}
>
<svelte:fragment slot="inner-end">
{#if noTokensMatch}
<button on:click={() => (filter = '')} aria-label={$i18n.tokens.manage.text.clear_filter}>
<IconClose />
</button>
{:else}
<IconSearch />
{/if}
</svelte:fragment>
</InputTextWithAction>
/>
</div>

{#if nonNullish($selectedNetwork)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
export let network: Network;
export let blackAndWhite = false;
export let size: LogoSize = 'xs';
export let size: LogoSize = 'xxs';
export let color: 'dust' | 'off-white' | 'white' = 'dust';
export let testId: string | undefined = undefined;
</script>
Expand Down
99 changes: 99 additions & 0 deletions src/frontend/src/lib/components/swap/Swap.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import { isNullish, nonNullish } from '@dfinity/utils';
import { BigNumber } from '@ethersproject/bignumber';
import { setContext } from 'svelte';
import { ICRC_CK_TOKENS_LEDGER_CANISTER_IDS } from '$env/networks/networks.icrc.env';
import type { Erc20ContractAddress, Erc20Token } from '$eth/types/erc20';
import { balance } from '$icp/api/icrc-ledger.api';
import type { LedgerCanisterIdText } from '$icp/types/canister';
import type { IcCkToken } from '$icp/types/ic-token';
import SwapButtonWithModal from '$lib/components/swap/SwapButtonWithModal.svelte';
import SwapModal from '$lib/components/swap/SwapModal.svelte';
import { allDisabledIcrcTokens } from '$lib/derived/all-tokens.derived';
import { authIdentity } from '$lib/derived/auth.derived';
import { modalSwap } from '$lib/derived/modal.derived';
import { nullishSignOut } from '$lib/services/auth.services';
import { exchangeRateERC20ToUsd, exchangeRateICRCToUsd } from '$lib/services/exchange.services';
import { balancesStore } from '$lib/stores/balances.store';
import { exchangeStore } from '$lib/stores/exchange.store';
import { modalStore } from '$lib/stores/modal.store';
import {
initSwapAmountsStore,
SWAP_AMOUNTS_CONTEXT_KEY,
type SwapAmountsContext
} from '$lib/stores/swap-amounts.store';
setContext<SwapAmountsContext>(SWAP_AMOUNTS_CONTEXT_KEY, {
store: initSwapAmountsStore()
});
const onOpenSwap = async (tokenId: symbol) => {
if (isNullish($authIdentity)) {
await nullishSignOut();
return;
}
modalStore.openSwap(tokenId);
const loadBalances = (): Promise<void[]> =>
Promise.all(
$allDisabledIcrcTokens.map(async ({ ledgerCanisterId, id }) => {
const icrcTokenBalance = await balance({
identity: $authIdentity,
owner: $authIdentity.getPrincipal(),
ledgerCanisterId
});
balancesStore.set({
tokenId: id,
data: {
data: BigNumber.from(icrcTokenBalance),
certified: true
}
});
})
);
const loadExchanges = async (): Promise<void> => {
const [currentErc20Prices, currentIcrcPrices] = await Promise.all([
exchangeRateERC20ToUsd(
$allDisabledIcrcTokens.reduce<Erc20ContractAddress[]>((acc, token) => {
const twinTokenAddress = (
(token as Partial<IcCkToken>).twinToken as Erc20Token | undefined
)?.address;
return nonNullish(twinTokenAddress)
? [
...acc,
{
address: twinTokenAddress
}
]
: acc;
}, [])
),
exchangeRateICRCToUsd(
$allDisabledIcrcTokens.reduce<LedgerCanisterIdText[]>(
(acc, { ledgerCanisterId }) =>
!ICRC_CK_TOKENS_LEDGER_CANISTER_IDS.includes(ledgerCanisterId)
? [...acc, ledgerCanisterId]
: acc,
[]
)
)
]);
exchangeStore.set([
...(nonNullish(currentErc20Prices) ? [currentErc20Prices] : []),
...(nonNullish(currentIcrcPrices) ? [currentIcrcPrices] : [])
]);
};
await loadBalances();
await loadExchanges();
};
</script>

<SwapButtonWithModal open={onOpenSwap} isOpen={$modalSwap}>
<SwapModal on:nnsClose />
</SwapButtonWithModal>
18 changes: 18 additions & 0 deletions src/frontend/src/lib/components/swap/SwapAmountExchange.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import { nonNullish } from '@dfinity/utils';
import IconArrowUpDown from '$lib/components/icons/lucide/IconArrowUpDown.svelte';
import type { OptionAmount } from '$lib/types/send';
import { formatUSD } from '$lib/utils/format.utils';
export let amount: OptionAmount;
export let exchangeRate: number | undefined;
let amountUSD: number | undefined;
$: amountUSD = nonNullish(amount) && nonNullish(exchangeRate) ? Number(amount) * exchangeRate : 0;
</script>

<div class="flex items-center gap-1">
<IconArrowUpDown size="14" />

<span>{nonNullish(exchangeRate) ? formatUSD({ value: amountUSD }) : '-'}</span>
</div>
Loading

0 comments on commit b6ef3e1

Please sign in to comment.